GitNexus/gitnexus/src/storage/gitnexus-managed-paths.ts
Gergő Magyar 170eefd4a0
fix(status): judge freshness by covered files, not a dirty working tree (#3083)
`gitnexus status` reported "stale (re-run gitnexus analyze)" whenever the
working tree held any modified or untracked file, including files the index
never reads. Because `analyze` cannot commit, stash or delete such a file,
the remedy it prescribed could not clear the verdict — the only way back to
up-to-date was to remove the file.

`meta.fileHashes` already records the exact set of files a run covered, so
answer the question directly: compare those hashes against disk, reusing
analyze's own scan, hash and diff helpers so the two cannot disagree about
what "changed" means. A new coverable file still counts as stale (the index
is genuinely incomplete then), but one `analyze` now settles it. The
repo-wide dirty flag survives only as the fallback for metadata written
before `fileHashes` existed.

Both freshness checks now read GitNexus's own analyze output (AGENTS.md,
CLAUDE.md, the agent skill mirrors) from one shared list. They previously
held separate copies, and since analyze rewrites those files after
recording hashes, a per-file comparison that missed them would report a
freshly indexed repository as permanently stale.

Closes #3077

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 12:56:41 +00:00

53 lines
2.2 KiB
TypeScript

/**
* The paths GitNexus itself writes during `analyze`.
*
* `analyze` rewrites the stats blocks in AGENTS.md/CLAUDE.md and refreshes the
* agent skill mirrors as its final step — after it has recorded the per-file
* hashes for the run. Counting its own output as a repository change makes
* every completed run look immediately out of date, which is the regression
* PR #1233 introduced and #1233's fix excluded these paths to prevent.
*
* Two freshness checks depend on this list agreeing: `isWorkingTreeDirty`
* (analyze's up-to-date fast-path gate) and the per-file comparison behind
* `status`. They used to hold separate copies of it, so a path added to one
* silently became a permanent "stale" verdict in the other. One list, imported
* by both.
*/
/**
* Repository-root-relative. A directory entry covers everything beneath it;
* a file entry matches only itself. Prefix collisions are NOT matches —
* `.agentsrc` is an ordinary file, not part of the `.agents` tree.
*/
export const GITNEXUS_MANAGED_PATHS = [
'.gitnexus',
'.claude',
'.cursor',
'.agents',
'AGENTS.md',
'CLAUDE.md',
] as const;
/**
* Git pathspecs excluding {@link GITNEXUS_MANAGED_PATHS} from a `git status`
* run rooted at the repository. Patterns include `./` so they match only at
* the repo root: a slash-free `:(exclude)AGENTS.md` would also drop
* `docs/AGENTS.md`, which {@link isGitNexusManagedPath} does not treat as
* managed. Both forms are emitted per entry: the root path itself, and `/**`
* for directory contents.
*/
export const GITNEXUS_MANAGED_PATH_EXCLUDES: readonly string[] = GITNEXUS_MANAGED_PATHS.flatMap(
(managed) => [`:(exclude,glob)./${managed}`, `:(exclude,glob)./${managed}/**`],
);
/**
* True when a repository-relative path is GitNexus's own output. Mirrors the
* pathspec semantics above: root-relative, whole path segments only, so
* neither `.agentsrc` nor a nested `subdir/.agents/` is treated as managed.
*/
export const isGitNexusManagedPath = (relPath: string): boolean => {
const normalized = relPath.replace(/\\/g, '/');
return GITNEXUS_MANAGED_PATHS.some(
(managed) => normalized === managed || normalized.startsWith(`${managed}/`),
);
};