gitnexus 1.6.13-rc.5 → 1.6.13-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/ai-context.js +1 -0
- package/dist/core/staleness-status.d.ts +43 -6
- package/dist/core/staleness-status.js +26 -8
- package/dist/mcp/local/local-backend.d.ts +28 -11
- package/dist/mcp/local/local-backend.js +33 -12
- package/dist/mcp/tools.js +14 -4
- package/dist/server/repo-projection.d.ts +12 -7
- package/dist/server/repo-projection.js +12 -7
- package/package.json +1 -1
- package/skills/gitnexus-debugging.md +1 -0
- package/skills/gitnexus-exploring.md +1 -0
- package/skills/gitnexus-guide.md +49 -0
- package/skills/gitnexus-impact-analysis.md +1 -0
- package/skills/gitnexus-refactoring.md +1 -0
package/dist/cli/ai-context.js
CHANGED
|
@@ -137,6 +137,7 @@ ${tableBody}`
|
|
|
137
137
|
This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows)`}.
|
|
138
138
|
|
|
139
139
|
> Index stale? Run \`${runner} analyze --index-only\` from the project root — it auto-selects an available runner. ${bootstrapNote}
|
|
140
|
+
> On query/context/impact/cypher object results, read staleness.status and branch/lastCommit. Re-analyze only for behind or diverged — current is clone HEAD, not main.
|
|
140
141
|
|
|
141
142
|
## Always Do
|
|
142
143
|
|
|
@@ -47,17 +47,51 @@ export interface StalenessInfo {
|
|
|
47
47
|
}
|
|
48
48
|
/** `info.status`, or the answer `isStale` implies for an info built without one. */
|
|
49
49
|
export declare const stalenessStatus: (info: StalenessInfo) => StalenessStatus;
|
|
50
|
+
/**
|
|
51
|
+
* The ref an index represents, as the resolved repo handle already knows it.
|
|
52
|
+
* `lastCommit` and `indexedAt` are always recorded on a handle; `branch` is
|
|
53
|
+
* best-effort — a plain analyze stamps the checked-out branch, but a detached
|
|
54
|
+
* HEAD, a non-git folder, or a legacy index that never recorded one leaves it
|
|
55
|
+
* absent (`run-analyze.ts`: `branchLabel ?? existingMeta?.branch`).
|
|
56
|
+
*/
|
|
57
|
+
export interface IndexedRef {
|
|
58
|
+
branch?: string;
|
|
59
|
+
lastCommit: string;
|
|
60
|
+
indexedAt: string;
|
|
61
|
+
}
|
|
50
62
|
/**
|
|
51
63
|
* The wire shape for staleness on every surface: MCP `list_repos`, the hot read
|
|
52
64
|
* tools, and the `serve` repo routes. One builder so one fact has one shape
|
|
53
65
|
* (#3232 review: "same sentinel as MCP").
|
|
54
66
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
67
|
+
* Two forms, chosen by whether the caller supplies a {@link IndexedRef}:
|
|
68
|
+
*
|
|
69
|
+
* - **Without a ref** — absent for `current`, as before. That is what
|
|
70
|
+
* `list_repos` and the `serve` routes emit; they already report the ref
|
|
71
|
+
* through their own top-level `branch` / `lastCommit` / `indexedAt` fields
|
|
72
|
+
* (#3226), so repeating it inside the payload would duplicate it.
|
|
73
|
+
* - **With a ref** — emitted for EVERY status, naming the index it describes.
|
|
74
|
+
* The hot read tools have nowhere else to put it: `attachToolStaleness` may
|
|
75
|
+
* add exactly one key to an arbitrary tool result. Without it `current` is
|
|
76
|
+
* indistinguishable between an index of the default branch and one of some
|
|
77
|
+
* feature branch, because `current` is a statement about a *ref*, not about
|
|
78
|
+
* the repository (#3291).
|
|
79
|
+
*
|
|
80
|
+
* `commitsBehind` is present only when git actually counted it, so `diverged`
|
|
81
|
+
* carries `status` and `hint` but no number — inventing one would be the silent
|
|
82
|
+
* wrong answer this exists to remove.
|
|
58
83
|
*/
|
|
59
84
|
export interface StalenessPayload {
|
|
60
|
-
status:
|
|
85
|
+
status: StalenessStatus;
|
|
86
|
+
/** Ref identity — present only on the ref-carrying (hot read tool) form. */
|
|
87
|
+
branch?: string;
|
|
88
|
+
lastCommit?: string;
|
|
89
|
+
indexedAt?: string;
|
|
90
|
+
/**
|
|
91
|
+
* What `commitsBehind` is counted against: the checked-out HEAD of the clone
|
|
92
|
+
* this index was built from, never the remote or the default branch.
|
|
93
|
+
*/
|
|
94
|
+
measuredAgainst?: 'HEAD';
|
|
61
95
|
commitsBehind?: number;
|
|
62
96
|
hint?: string;
|
|
63
97
|
}
|
|
@@ -66,9 +100,12 @@ export interface StalenessPayload {
|
|
|
66
100
|
* nothing to report.
|
|
67
101
|
*
|
|
68
102
|
* `unknown` is emitted only when `includeUnknown` is set. A listing a monitor
|
|
69
|
-
* reads wants it; the hot
|
|
70
|
-
* no history to measure and would otherwise repeat that on every
|
|
103
|
+
* reads wants it; the no-ref hot-tool form does not, because a `--skip-git`
|
|
104
|
+
* folder has no history to measure and would otherwise repeat that on every
|
|
105
|
+
* response. The ref-carrying form reports it regardless — which index answered
|
|
106
|
+
* is knowable even when its freshness is not.
|
|
71
107
|
*/
|
|
72
108
|
export declare const stalenessPayload: (info: StalenessInfo | undefined, opts?: {
|
|
73
109
|
includeUnknown?: boolean;
|
|
110
|
+
ref?: IndexedRef;
|
|
74
111
|
}) => StalenessPayload | undefined;
|
|
@@ -14,19 +14,37 @@ export const stalenessStatus = (info) => info.status ?? (info.isStale ? 'behind'
|
|
|
14
14
|
* nothing to report.
|
|
15
15
|
*
|
|
16
16
|
* `unknown` is emitted only when `includeUnknown` is set. A listing a monitor
|
|
17
|
-
* reads wants it; the hot
|
|
18
|
-
* no history to measure and would otherwise repeat that on every
|
|
17
|
+
* reads wants it; the no-ref hot-tool form does not, because a `--skip-git`
|
|
18
|
+
* folder has no history to measure and would otherwise repeat that on every
|
|
19
|
+
* response. The ref-carrying form reports it regardless — which index answered
|
|
20
|
+
* is knowable even when its freshness is not.
|
|
19
21
|
*/
|
|
20
22
|
export const stalenessPayload = (info, opts = {}) => {
|
|
21
23
|
if (!info)
|
|
22
24
|
return undefined;
|
|
23
25
|
const status = stalenessStatus(info);
|
|
24
26
|
const hint = info.hint ? { hint: info.hint } : {};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// No ref: bit-identical to the pre-#3291 output for every status. This is
|
|
28
|
+
// what keeps `list_repos` and both `serve` routes byte-stable, and their
|
|
29
|
+
// exact-match tests passing unmodified.
|
|
30
|
+
if (!opts.ref) {
|
|
31
|
+
if (status === 'current')
|
|
32
|
+
return undefined;
|
|
33
|
+
if (status === 'unknown')
|
|
34
|
+
return opts.includeUnknown ? { status } : undefined;
|
|
35
|
+
if (status === 'diverged')
|
|
36
|
+
return { status, ...hint };
|
|
37
|
+
return { status, commitsBehind: info.commitsBehind, ...hint };
|
|
38
|
+
}
|
|
39
|
+
const ref = {
|
|
40
|
+
...(opts.ref.branch ? { branch: opts.ref.branch } : {}),
|
|
41
|
+
lastCommit: opts.ref.lastCommit,
|
|
42
|
+
indexedAt: opts.ref.indexedAt,
|
|
43
|
+
measuredAgainst: 'HEAD',
|
|
44
|
+
};
|
|
45
|
+
if (status === 'current' || status === 'unknown')
|
|
46
|
+
return { status, ...ref };
|
|
29
47
|
if (status === 'diverged')
|
|
30
|
-
return { status, ...hint };
|
|
31
|
-
return { status, commitsBehind: info.commitsBehind, ...hint };
|
|
48
|
+
return { status, ...ref, ...hint };
|
|
49
|
+
return { status, ...ref, commitsBehind: info.commitsBehind, ...hint };
|
|
32
50
|
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { isTestFilePath } from '../../core/ingestion/utils/test-file-path.js';
|
|
9
9
|
import { type RegistryEntry, type BranchSummary } from '../../storage/repo-manager.js';
|
|
10
10
|
import { GroupService } from '../../core/group/service.js';
|
|
11
|
-
import { type StalenessInfo, type StalenessPayload } from '../../core/staleness-status.js';
|
|
11
|
+
import { type IndexedRef, type StalenessInfo, type StalenessPayload } from '../../core/staleness-status.js';
|
|
12
12
|
/** Shared predicate; re-exported so MCP importers keep the old public name. */
|
|
13
13
|
export { isTestFilePath };
|
|
14
14
|
/** Valid LadybugDB node labels for safe Cypher query construction */
|
|
@@ -186,7 +186,11 @@ interface RepoHandle {
|
|
|
186
186
|
lastCommit: string;
|
|
187
187
|
remoteUrl?: string;
|
|
188
188
|
stats?: RegistryEntry['stats'];
|
|
189
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Branch this handle's index describes (#2106/#3291). The flat/workspace
|
|
191
|
+
* slot keeps the primary checkout name; `applyBranchScope` overwrites it
|
|
192
|
+
* with the pin when serving a `branches[]` sub-index.
|
|
193
|
+
*/
|
|
190
194
|
branch?: string;
|
|
191
195
|
/** Pinned `--branch` sub-indexes available for this repo, distinct from the flat workspace slot (#2106/#2354). */
|
|
192
196
|
branches?: BranchSummary[];
|
|
@@ -263,17 +267,24 @@ export interface ListReposPagination {
|
|
|
263
267
|
nextOffset?: number;
|
|
264
268
|
}
|
|
265
269
|
/**
|
|
266
|
-
* #2655: attach a non-blocking `staleness` signal to a tool result
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* {@link canCarryStaleness}) — it never changes an existing result's shape.
|
|
270
|
+
* #2655: attach a non-blocking `staleness` signal to a tool result. Only ever
|
|
271
|
+
* ADDS a field to a carryable object result (see {@link canCarryStaleness}) —
|
|
272
|
+
* it never changes an existing result's shape.
|
|
270
273
|
*
|
|
271
|
-
* `
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
274
|
+
* #3291: `ref` names the index the answer came from. Supplying it switches the
|
|
275
|
+
* payload to the ref-carrying form, which reports every status — including
|
|
276
|
+
* `current` and `unknown` — because that one added key is the only place a tool
|
|
277
|
+
* result can say WHICH index answered. Absence used to be the freshness signal
|
|
278
|
+
* here; it could not distinguish a current index of the default branch from a
|
|
279
|
+
* current index of some feature branch, since `current` is a statement about a
|
|
280
|
+
* ref rather than about the repository.
|
|
281
|
+
*
|
|
282
|
+
* With no `ref` the pre-#3291 behaviour is unchanged: absent for `current`,
|
|
283
|
+
* `diverged` attached as a positive finding that the index is not at HEAD, and
|
|
284
|
+
* `unknown` withheld as noise (#3256). A missing `info` still attaches nothing
|
|
285
|
+
* either way, which is what keeps a failed freshness probe non-fatal.
|
|
275
286
|
*/
|
|
276
|
-
export declare function attachToolStaleness(result: unknown, info: StalenessInfo | undefined): unknown;
|
|
287
|
+
export declare function attachToolStaleness(result: unknown, info: StalenessInfo | undefined, ref?: IndexedRef): unknown;
|
|
277
288
|
export declare class LocalBackend {
|
|
278
289
|
private static readonly TOOL_STALENESS_TTL_MS;
|
|
279
290
|
private repos;
|
|
@@ -567,6 +578,12 @@ export declare class LocalBackend {
|
|
|
567
578
|
* skipping the `git` spawn entirely for results that can't carry it (error
|
|
568
579
|
* envelopes, arrays, non-objects — see {@link canCarryStaleness}) so an
|
|
569
580
|
* error-returning call pays nothing.
|
|
581
|
+
*
|
|
582
|
+
* #3291: the ref comes straight off the already-resolved handle, so naming
|
|
583
|
+
* the index costs no extra I/O — no git spawn, no metadata read, and the
|
|
584
|
+
* `stalenessForTool` TTL cache is untouched. `branch` is passed through as-is
|
|
585
|
+
* and is legitimately absent for a detached HEAD or a legacy index; the
|
|
586
|
+
* always-present `lastCommit` is what identifies the index in that case.
|
|
570
587
|
*/
|
|
571
588
|
private withToolStaleness;
|
|
572
589
|
/**
|
|
@@ -925,18 +925,25 @@ function canCarryStaleness(result) {
|
|
|
925
925
|
!('staleness' in result));
|
|
926
926
|
}
|
|
927
927
|
/**
|
|
928
|
-
* #2655: attach a non-blocking `staleness` signal to a tool result
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* {@link canCarryStaleness}) — it never changes an existing result's shape.
|
|
928
|
+
* #2655: attach a non-blocking `staleness` signal to a tool result. Only ever
|
|
929
|
+
* ADDS a field to a carryable object result (see {@link canCarryStaleness}) —
|
|
930
|
+
* it never changes an existing result's shape.
|
|
932
931
|
*
|
|
933
|
-
* `
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
932
|
+
* #3291: `ref` names the index the answer came from. Supplying it switches the
|
|
933
|
+
* payload to the ref-carrying form, which reports every status — including
|
|
934
|
+
* `current` and `unknown` — because that one added key is the only place a tool
|
|
935
|
+
* result can say WHICH index answered. Absence used to be the freshness signal
|
|
936
|
+
* here; it could not distinguish a current index of the default branch from a
|
|
937
|
+
* current index of some feature branch, since `current` is a statement about a
|
|
938
|
+
* ref rather than about the repository.
|
|
939
|
+
*
|
|
940
|
+
* With no `ref` the pre-#3291 behaviour is unchanged: absent for `current`,
|
|
941
|
+
* `diverged` attached as a positive finding that the index is not at HEAD, and
|
|
942
|
+
* `unknown` withheld as noise (#3256). A missing `info` still attaches nothing
|
|
943
|
+
* either way, which is what keeps a failed freshness probe non-fatal.
|
|
937
944
|
*/
|
|
938
|
-
export function attachToolStaleness(result, info) {
|
|
939
|
-
const staleness = stalenessPayload(info);
|
|
945
|
+
export function attachToolStaleness(result, info, ref) {
|
|
946
|
+
const staleness = stalenessPayload(info, ref ? { ref } : {});
|
|
940
947
|
if (!staleness || !canCarryStaleness(result)) {
|
|
941
948
|
return result;
|
|
942
949
|
}
|
|
@@ -1513,6 +1520,8 @@ export class LocalBackend {
|
|
|
1513
1520
|
indexedAt: summary.indexedAt,
|
|
1514
1521
|
lastCommit: summary.lastCommit,
|
|
1515
1522
|
stats: summary.stats,
|
|
1523
|
+
// The handle now represents the pin, not the flat slot (#3291).
|
|
1524
|
+
branch: summary.branch,
|
|
1516
1525
|
};
|
|
1517
1526
|
}
|
|
1518
1527
|
// Stale summary (sub-index adopted/deleted): refresh so later calls see
|
|
@@ -1994,6 +2003,12 @@ export class LocalBackend {
|
|
|
1994
2003
|
* skipping the `git` spawn entirely for results that can't carry it (error
|
|
1995
2004
|
* envelopes, arrays, non-objects — see {@link canCarryStaleness}) so an
|
|
1996
2005
|
* error-returning call pays nothing.
|
|
2006
|
+
*
|
|
2007
|
+
* #3291: the ref comes straight off the already-resolved handle, so naming
|
|
2008
|
+
* the index costs no extra I/O — no git spawn, no metadata read, and the
|
|
2009
|
+
* `stalenessForTool` TTL cache is untouched. `branch` is passed through as-is
|
|
2010
|
+
* and is legitimately absent for a detached HEAD or a legacy index; the
|
|
2011
|
+
* always-present `lastCommit` is what identifies the index in that case.
|
|
1997
2012
|
*/
|
|
1998
2013
|
async withToolStaleness(repo, result) {
|
|
1999
2014
|
if (!canCarryStaleness(result))
|
|
@@ -2001,9 +2016,15 @@ export class LocalBackend {
|
|
|
2001
2016
|
// Defensive: `checkStalenessAsync` self-catches today, but a rejection here
|
|
2002
2017
|
// must never fail the tool — degrade to no-staleness. Paired with the
|
|
2003
2018
|
// evict-on-reject in `stalenessForTool`, a transient failure also can't
|
|
2004
|
-
// poison the TTL cache entry (#2655 review F1).
|
|
2019
|
+
// poison the TTL cache entry (#2655 review F1). A rejection leaves `info`
|
|
2020
|
+
// undefined, and the builder returns nothing for that even with a ref, so
|
|
2021
|
+
// the degraded path still attaches no field.
|
|
2005
2022
|
const staleness = await this.stalenessForTool(repo).catch(() => undefined);
|
|
2006
|
-
return attachToolStaleness(result, staleness
|
|
2023
|
+
return attachToolStaleness(result, staleness, {
|
|
2024
|
+
branch: repo.branch,
|
|
2025
|
+
lastCommit: repo.lastCommit,
|
|
2026
|
+
indexedAt: repo.indexedAt,
|
|
2027
|
+
});
|
|
2007
2028
|
}
|
|
2008
2029
|
/**
|
|
2009
2030
|
* #2655: commits-behind freshness for the hot read tools, deduped per index.
|
package/dist/mcp/tools.js
CHANGED
|
@@ -51,6 +51,8 @@ export const PDG_QUERY_MAX_LIMIT = 200;
|
|
|
51
51
|
export const IMPACT_MAX_DEPTH = 32;
|
|
52
52
|
const CWD_AWARE_REPO_OMISSION = 'Omit when only one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing an unindexed nested Git checkout; otherwise specify it explicitly.';
|
|
53
53
|
const MUTATING_REPO_OMISSION = 'Omit only when one repo is indexed or an MCP default is configured; otherwise mutating tools require an explicit repo.';
|
|
54
|
+
/** Always-on identity+freshness field on query/context/impact/cypher object results (#3291). */
|
|
55
|
+
const HOT_READ_STALENESS_NOTE = "Object results attach `staleness` even when current. Read `staleness.branch`/`lastCommit` for which index answered and `status` for freshness. Re-analyze only for `behind` or `diverged` — `current` is this clone's HEAD, not necessarily the default branch; `unknown` is unmeasurable, not stale. Field is only on object results (not raw-array cypher, error envelopes, or `@group` calls).";
|
|
54
56
|
export const GITNEXUS_TOOLS = [
|
|
55
57
|
{
|
|
56
58
|
name: 'list_repos',
|
|
@@ -105,7 +107,9 @@ Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank
|
|
|
105
107
|
|
|
106
108
|
GROUP MODE: set "repo" to "@<groupName>" to search all member repos in that group (merged via RRF), or "@<groupName>/<groupRepoPath>" to run against a single member (same path keys as in group.yaml). If you use "@<groupName>" only, the member repo defaults to the lexicographically first key in group.yaml "repos". Prefer resources for contracts/status (see migration from legacy group_* tools).
|
|
107
109
|
|
|
108
|
-
SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). When "repo" starts with "@", only processes whose symbols fall under that prefix are included. For a normal indexed repo name (no leading @), this field is currently ignored by the server
|
|
110
|
+
SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). When "repo" starts with "@", only processes whose symbols fall under that prefix are included. For a normal indexed repo name (no leading @), this field is currently ignored by the server.
|
|
111
|
+
|
|
112
|
+
${HOT_READ_STALENESS_NOTE}`,
|
|
109
113
|
annotations: QUERY_TOOL_ANNOTATIONS,
|
|
110
114
|
inputSchema: {
|
|
111
115
|
type: 'object',
|
|
@@ -212,7 +216,9 @@ TIPS:
|
|
|
212
216
|
- Community = auto-detected functional area (Leiden algorithm). Properties: heuristicLabel, cohesion, symbolCount, keywords, description, enrichedBy
|
|
213
217
|
- Process = execution flow trace from entry point to terminal. Properties: heuristicLabel, processType, stepCount, communities, entryPointId, terminalId
|
|
214
218
|
- Use heuristicLabel (not label) for human-readable community/process names
|
|
215
|
-
- PDG layers (only when indexed with \`--pdg\`): BasicBlock nodes + CFG / CDG (control dependence, branch sense 'T'|'F' in reason) / REACHING_DEF (def→use, variable in reason) edges, all BasicBlock→BasicBlock. Prefer the \`pdg_query\` tool — it anchors + bounds these for you (raw \`[:CDG*]\`/\`[:REACHING_DEF*]\` path scans are unindexed and unbounded)
|
|
219
|
+
- PDG layers (only when indexed with \`--pdg\`): BasicBlock nodes + CFG / CDG (control dependence, branch sense 'T'|'F' in reason) / REACHING_DEF (def→use, variable in reason) edges, all BasicBlock→BasicBlock. Prefer the \`pdg_query\` tool — it anchors + bounds these for you (raw \`[:CDG*]\`/\`[:REACHING_DEF*]\` path scans are unindexed and unbounded).
|
|
220
|
+
|
|
221
|
+
${HOT_READ_STALENESS_NOTE}`,
|
|
216
222
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
217
223
|
inputSchema: {
|
|
218
224
|
type: 'object',
|
|
@@ -264,7 +270,9 @@ REQUIRES RE-INDEX: causes.scopeExtractionFiles, causes.receiverTyping, causes.ex
|
|
|
264
270
|
|
|
265
271
|
GROUP MODE: set "repo" to "@<groupName>" to run context in each member repo (aggregated list), or "@<groupName>/<groupRepoPath>" for one member. If you use "@<groupName>" only, the member defaults to the lexicographically first key in group.yaml "repos".
|
|
266
272
|
|
|
267
|
-
SERVICE: optional monorepo path prefix (case-sensitive path segments). When "repo" starts with "@", prefix-matches resolved symbol file paths; when a hit is outside the prefix, that member returns an empty payload for the symbol. Ignored for a normal indexed repo name
|
|
273
|
+
SERVICE: optional monorepo path prefix (case-sensitive path segments). When "repo" starts with "@", prefix-matches resolved symbol file paths; when a hit is outside the prefix, that member returns an empty payload for the symbol. Ignored for a normal indexed repo name.
|
|
274
|
+
|
|
275
|
+
${HOT_READ_STALENESS_NOTE}`,
|
|
268
276
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
269
277
|
inputSchema: {
|
|
270
278
|
type: 'object',
|
|
@@ -473,7 +481,9 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match
|
|
|
473
481
|
|
|
474
482
|
GROUP MODE: set "repo" to "@<groupName>" for cross-repo impact anchored at the default member (lexicographically first key in group.yaml "repos"), or "@<groupName>/<groupRepoPath>" to choose the member (same path keys as in group.yaml). Phase-1 walk runs in that member; cross-boundary fan-out uses the group bridge. A cross entry with fanout_status:"not_attempted" proves the declared repository boundary, but its far endpoint has no graph symbol; do not interpret empty by_depth or affected_processes on that entry as a completed zero-impact walk. The fan-out attempts at most 50 neighbour crossings, strongest-confidence first. Any short answer carries truncated:true, truncatedRepos, riskEpistemic:"lower-bound" AND a truncationReason — dropping a crossing can only move risk DOWN, so treat that risk as a floor, never as a verdict. truncated:true does NOT always mean the fan-out ran out of room, so branch on truncationReason: the remedy differs. 'timeout' (the fan-out's wall-clock budget expired) and 'partial' (a neighbour crossing, or the local walk, was cut short) are runtime limits — the same query can return more on a retry or with a larger timeoutMs. 'incomplete-sync' is structural: the group bridge was built by a sync that could not say which repos it read, or that could not read an in-scope repo, so those repos' contracts are absent from EVERY query against this bridge, and truncatedRepos names them even when ZERO crossings to them were attempted. Retrying returns the same floor — run group_sync (\`gitnexus group sync\`) and query again. 'suppressed-stage' is also structural but has a DIFFERENT remedy: the sync was asked to skip a matching stage (\`--exact-only\` / exactOnly), so cross-links that stage would have found are absent BY REQUEST. Re-running the sync unchanged returns the same floor — re-run it WITHOUT that flag. Do not report a repo as broken for this reason; nothing failed to read.
|
|
475
483
|
|
|
476
|
-
SERVICE: optional monorepo path prefix (case-sensitive path segments). When "repo" starts with "@", scopes the local impact walk and cross-repo symbol paths to files under that prefix; ignored for a normal indexed repo name
|
|
484
|
+
SERVICE: optional monorepo path prefix (case-sensitive path segments). When "repo" starts with "@", scopes the local impact walk and cross-repo symbol paths to files under that prefix; ignored for a normal indexed repo name.
|
|
485
|
+
|
|
486
|
+
${HOT_READ_STALENESS_NOTE}`,
|
|
477
487
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
478
488
|
inputSchema: {
|
|
479
489
|
type: 'object',
|
|
@@ -21,13 +21,18 @@ export interface RepoProjectionSource {
|
|
|
21
21
|
* Staleness through the shared {@link stalenessPayload} builder, so this route
|
|
22
22
|
* and MCP `list_repos` emit one shape for one fact (#3232 review, #3256).
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
24
|
+
* This listing/HTTP helper uses the no-ref form: absent when the index is
|
|
25
|
+
* current; `unknown` is included via `includeUnknown`. Otherwise
|
|
26
|
+
* `staleness.status` says what git could establish: `behind` with the counted
|
|
27
|
+
* `commitsBehind`; `diverged` when HEAD has provably moved off the indexed
|
|
28
|
+
* commit but the history needed to count the gap is gone — the state a
|
|
29
|
+
* branch-pinned `url` clone reaches once git prunes the commit a failed
|
|
30
|
+
* re-index left behind; or `unknown` when the repository could not be
|
|
31
|
+
* measured at all.
|
|
32
|
+
*
|
|
33
|
+
* Hot read tools (`query`/`context`/`impact`/`cypher`) use the ref-carrying
|
|
34
|
+
* form: they emit `unknown` (and `current`) with `branch?`/`lastCommit`/
|
|
35
|
+
* `indexedAt`/`measuredAgainst`.
|
|
31
36
|
*
|
|
32
37
|
* All of it measures the index against the local working tree — the same thing
|
|
33
38
|
* `gitnexus status` and MCP `list_repos` measure — not against the remote.
|
|
@@ -14,13 +14,18 @@ import { stalenessPayload, } from '../core/staleness-status.js';
|
|
|
14
14
|
* Staleness through the shared {@link stalenessPayload} builder, so this route
|
|
15
15
|
* and MCP `list_repos` emit one shape for one fact (#3232 review, #3256).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* This listing/HTTP helper uses the no-ref form: absent when the index is
|
|
18
|
+
* current; `unknown` is included via `includeUnknown`. Otherwise
|
|
19
|
+
* `staleness.status` says what git could establish: `behind` with the counted
|
|
20
|
+
* `commitsBehind`; `diverged` when HEAD has provably moved off the indexed
|
|
21
|
+
* commit but the history needed to count the gap is gone — the state a
|
|
22
|
+
* branch-pinned `url` clone reaches once git prunes the commit a failed
|
|
23
|
+
* re-index left behind; or `unknown` when the repository could not be
|
|
24
|
+
* measured at all.
|
|
25
|
+
*
|
|
26
|
+
* Hot read tools (`query`/`context`/`impact`/`cypher`) use the ref-carrying
|
|
27
|
+
* form: they emit `unknown` (and `current`) with `branch?`/`lastCommit`/
|
|
28
|
+
* `indexedAt`/`measuredAgainst`.
|
|
24
29
|
*
|
|
25
30
|
* All of it measures the index against the local working tree — the same thing
|
|
26
31
|
* `gitnexus status` and MCP `list_repos` measure — not against the remote.
|
package/package.json
CHANGED
|
@@ -42,6 +42,7 @@ diagnosis.
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
45
|
+
> Hot-tool `staleness` names which index answered (`branch`/`lastCommit`) and how fresh it is (`status`). Re-analyze only for `behind` or `diverged` — `current` is identity, `unknown` is unmeasurable.
|
|
45
46
|
|
|
46
47
|
## Checklist
|
|
47
48
|
|
|
@@ -36,6 +36,7 @@ the bound repository and index freshness alongside your explanation.
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
> If step 2 says "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
39
|
+
> Hot-tool `staleness` names which index answered (`branch`/`lastCommit`) and how fresh it is (`status`). Re-analyze only for `behind` or `diverged` — `current` is identity, `unknown` is unmeasurable.
|
|
39
40
|
|
|
40
41
|
## Checklist
|
|
41
42
|
|
package/skills/gitnexus-guide.md
CHANGED
|
@@ -16,6 +16,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
|
|
16
16
|
3. **Follow the skill's workflow and checklist**
|
|
17
17
|
|
|
18
18
|
> If step 1 warns the index is stale, run `node .gitnexus/run.cjs analyze` in the terminal first.
|
|
19
|
+
> On `query` / `context` / `impact` / `cypher`, read `staleness.status` and `staleness.branch`/`lastCommit` before using the answer. Re-analyze only for `behind` or `diverged`.
|
|
19
20
|
|
|
20
21
|
## Skills
|
|
21
22
|
|
|
@@ -81,6 +82,54 @@ list_repos { offset: 400 } → repos 401–437, hasMore false
|
|
|
81
82
|
|
|
82
83
|
Notes: `offset` ≥ `total` returns an empty page (with `total` still reported). Out-of-range or malformed `limit`/`offset` (non-integer, `limit` outside `[1, 200]`, `offset < 0`) are rejected with a clear error — `limit` above the max is rejected, not silently capped. The order is deterministic (lower-cased name, then path), so paging never skips or duplicates an entry while the registry is unchanged.
|
|
83
84
|
|
|
85
|
+
### Inline staleness signal (`query` / `context` / `impact` / `cypher`)
|
|
86
|
+
|
|
87
|
+
These four hot read tools attach a non-blocking `staleness` field to every response, in the shape `{ status, branch?, lastCommit, indexedAt, measuredAgainst, commitsBehind?, hint? }`. It answers two different questions at once: **which index answered** and **how fresh it is**. The identity half is why the field is present even when nothing is wrong — an answer computed from a branch-pinned index is otherwise indistinguishable from one computed from the default branch (#3291):
|
|
88
|
+
|
|
89
|
+
```jsonc
|
|
90
|
+
{ /* …the tool's normal result… */
|
|
91
|
+
"staleness": {
|
|
92
|
+
"status": "current",
|
|
93
|
+
"branch": "feature/checkout-v2",
|
|
94
|
+
"lastCommit": "4f2a1c9e8b7d6a5c4e3f2a1b0c9d8e7f6a5b4c3d",
|
|
95
|
+
"indexedAt": "2026-09-15T07:12:00.000Z",
|
|
96
|
+
"measuredAgainst": "HEAD"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`status: "current"` here means *this index is at the HEAD of the clone it was built from* — not that it is current with the default branch. `measuredAgainst` names what `commitsBehind` is counted against: the checked-out HEAD of that clone, never the remote. `branch` is the branch the index represents; it is absent for a detached HEAD, a non-git folder, or a legacy index that never recorded one, so read `lastCommit` when you need an identifier that is always present.
|
|
102
|
+
|
|
103
|
+
When the index is behind that HEAD, the count and hint ride along:
|
|
104
|
+
|
|
105
|
+
```jsonc
|
|
106
|
+
{ /* …the tool's normal result… */
|
|
107
|
+
"staleness": {
|
|
108
|
+
"status": "behind", "commitsBehind": 3, "branch": "main",
|
|
109
|
+
"lastCommit": "a0c945022d06b8815f93ffd8838df9ed5c08cbc0",
|
|
110
|
+
"indexedAt": "2026-09-04T20:45:47.481Z", "measuredAgainst": "HEAD",
|
|
111
|
+
"hint": "⚠️ Index is 3 commits behind HEAD. Run analyze tool to update."
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`commitsBehind` is present only when git counted the gap. When git could not count it but HEAD still resolves to a commit other than the indexed one — usually because the indexed commit is no longer in the clone's history — the index is provably not at HEAD with no countable gap, so no number is reported:
|
|
117
|
+
|
|
118
|
+
```jsonc
|
|
119
|
+
{ /* …the tool's normal result… */
|
|
120
|
+
"staleness": {
|
|
121
|
+
"status": "diverged", "branch": "main",
|
|
122
|
+
"lastCommit": "a0c945022d06b8815f93ffd8838df9ed5c08cbc0",
|
|
123
|
+
"indexedAt": "2026-09-04T20:45:47.481Z", "measuredAgainst": "HEAD",
|
|
124
|
+
"hint": "⚠️ Index is not at HEAD and the commit gap could not be counted — the recorded commit may no longer be in this clone's history. Run analyze tool to update."
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
So: **read `status` before using `commitsBehind`**, and read `branch`/`lastCommit` before assuming which ref the answer describes. `status: "unknown"` means the freshness check could not run at all (a `--skip-git` folder has no history to measure) — the ref is still reported, because which index answered is knowable even when its freshness is not. The field is only ever added to object results — raw-array `cypher` output and error envelopes are returned unchanged. `@group`-targeted calls do not carry it (multi-repo staleness is ill-defined). Re-run `analyze` only for `behind` or `diverged` — those mean the index is not at this clone's HEAD. `unknown` is unmeasurable, not stale; analyze cannot make it `current` unless git history exists.
|
|
130
|
+
|
|
131
|
+
`list_repos` and the HTTP repo routes are unchanged: they omit `staleness` entirely for a current index and report the ref through their own top-level `branch` / `lastCommit` / `indexedAt` fields.
|
|
132
|
+
|
|
84
133
|
### Taint findings (`explain`)
|
|
85
134
|
|
|
86
135
|
`explain` returns taint findings recorded by `gitnexus analyze --pdg` — intra-procedural `TAINTED` edges plus cross-function `TAINT_PATH` hops where the interprocedural taint phase found a function-level source→sink chain. Each finding includes a sink category (command-injection, code-injection, path-traversal, sql-injection, xss), source/sink lines, and the ordered hop path with the variable carried on each hop.
|
|
@@ -53,6 +53,7 @@ Repository: <name> (<path>) Worktree: <path> Index: <commit>, <n> behind HEA
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
56
|
+
> Hot-tool `staleness` names which index answered (`branch`/`lastCommit`) and how fresh it is (`status`). Re-analyze only for `behind` or `diverged` — `current` is identity, `unknown` is unmeasurable.
|
|
56
57
|
> If `.gitnexus/run.cjs` is missing, replace `node .gitnexus/run.cjs` with `npx gitnexus` in the fallback commands.
|
|
57
58
|
|
|
58
59
|
## Checklist
|
|
@@ -46,6 +46,7 @@ checkout and reports nothing changed, which reads as a verified refactor.
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
49
|
+
> Hot-tool `staleness` names which index answered (`branch`/`lastCommit`) and how fresh it is (`status`). Re-analyze only for `behind` or `diverged` — `current` is identity, `unknown` is unmeasurable.
|
|
49
50
|
|
|
50
51
|
## Checklists
|
|
51
52
|
|