mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(cli): make Claude skills discoverable (#2434)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
This commit is contained in:
parent
e3136f593f
commit
a05b501102
26 changed files with 583 additions and 161 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -97,6 +97,12 @@ gitnexus/vendor/**/node_modules/
|
|||
.claude/helpers
|
||||
.claude/skills/*
|
||||
!.claude/skills/gitnexus/
|
||||
!.claude/skills/gitnexus-cli/
|
||||
!.claude/skills/gitnexus-debugging/
|
||||
!.claude/skills/gitnexus-exploring/
|
||||
!.claude/skills/gitnexus-guide/
|
||||
!.claude/skills/gitnexus-impact-analysis/
|
||||
!.claude/skills/gitnexus-refactoring/
|
||||
!.claude/skills/gitnexus-pr-swarm-review/
|
||||
|
||||
.history/
|
||||
|
|
|
|||
54
AGENTS.md
54
AGENTS.md
|
|
@ -41,7 +41,7 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING.
|
|||
- **[ARCHITECTURE.md](ARCHITECTURE.md)**, **[CONTRIBUTING.md](CONTRIBUTING.md)**, **[GUARDRAILS.md](GUARDRAILS.md)**
|
||||
- **Call & inheritance resolution (RFC #909 Ring 3):** See ARCHITECTURE.md § Scope-Resolution Pipeline. All languages resolve calls and inheritance through the scope-resolution pipeline (`Registry.lookup`, `preEmitInheritanceEdges`, `emitHeritageEdges`, `buildMro` → `MethodDispatchIndex`). **Shared code in `gitnexus/src/core/ingestion/` must not name languages** — plug language behavior in via `LanguageProvider` / `ScopeResolver` hooks. A language plugs in by implementing `ScopeResolver` (`scope-resolution/contract/scope-resolver.ts`) and registering it in `SCOPE_RESOLVERS`. (The legacy call-resolution DAG + `@heritage` capture path were removed in RING4-1 #942.)
|
||||
- **Cursor:** `.cursor/index.mdc` (always-on); `.cursor/rules/*.mdc` (glob-scoped). Legacy `.cursorrules` deprecated.
|
||||
- **GitNexus:** skills in `.claude/skills/gitnexus/`; MCP rules in `gitnexus:start` block below.
|
||||
- **GitNexus:** standard skills in `.claude/skills/gitnexus-*/`; MCP rules in `gitnexus:start` block below.
|
||||
|
||||
## PR Swarm Review (cross-CLI)
|
||||
|
||||
|
|
@ -106,32 +106,32 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati
|
|||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (239 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
|
||||
| Work in the Extractors area (135 symbols) | `.claude/skills/generated/extractors/SKILL.md` |
|
||||
| Work in the Components area (112 symbols) | `.claude/skills/generated/components/SKILL.md` |
|
||||
| Work in the Lbug area (96 symbols) | `.claude/skills/generated/lbug/SKILL.md` |
|
||||
| Work in the Group area (94 symbols) | `.claude/skills/generated/group/SKILL.md` |
|
||||
| Work in the Cli area (92 symbols) | `.claude/skills/generated/cli/SKILL.md` |
|
||||
| Work in the Configs area (92 symbols) | `.claude/skills/generated/configs/SKILL.md` |
|
||||
| Work in the Type-extractors area (90 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
|
||||
| Work in the Hooks area (88 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
|
||||
| Work in the Unit area (80 symbols) | `.claude/skills/generated/unit/SKILL.md` |
|
||||
| Work in the Cpp area (73 symbols) | `.claude/skills/generated/cpp/SKILL.md` |
|
||||
| Work in the Scope-resolution area (72 symbols) | `.claude/skills/generated/scope-resolution/SKILL.md` |
|
||||
| Work in the Server area (66 symbols) | `.claude/skills/generated/server/SKILL.md` |
|
||||
| Work in the Local area (61 symbols) | `.claude/skills/generated/local/SKILL.md` |
|
||||
| Work in the Wiki area (60 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
|
||||
| Work in the Workers area (57 symbols) | `.claude/skills/generated/workers/SKILL.md` |
|
||||
| Work in the Embeddings area (56 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
|
||||
| Work in the Typescript area (53 symbols) | `.claude/skills/generated/typescript/SKILL.md` |
|
||||
| Work in the Storage area (51 symbols) | `.claude/skills/generated/storage/SKILL.md` |
|
||||
| Work in the Php area (48 symbols) | `.claude/skills/generated/php/SKILL.md` |
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (239 symbols) | `.claude/skills/gitnexus-area-ingestion/SKILL.md` |
|
||||
| Work in the Extractors area (135 symbols) | `.claude/skills/gitnexus-area-extractors/SKILL.md` |
|
||||
| Work in the Components area (112 symbols) | `.claude/skills/gitnexus-area-components/SKILL.md` |
|
||||
| Work in the Lbug area (96 symbols) | `.claude/skills/gitnexus-area-lbug/SKILL.md` |
|
||||
| Work in the Group area (94 symbols) | `.claude/skills/gitnexus-area-group/SKILL.md` |
|
||||
| Work in the Cli area (92 symbols) | `.claude/skills/gitnexus-area-cli/SKILL.md` |
|
||||
| Work in the Configs area (92 symbols) | `.claude/skills/gitnexus-area-configs/SKILL.md` |
|
||||
| Work in the Type-extractors area (90 symbols) | `.claude/skills/gitnexus-area-type-extractors/SKILL.md` |
|
||||
| Work in the Hooks area (88 symbols) | `.claude/skills/gitnexus-area-hooks/SKILL.md` |
|
||||
| Work in the Unit area (80 symbols) | `.claude/skills/gitnexus-area-unit/SKILL.md` |
|
||||
| Work in the Cpp area (73 symbols) | `.claude/skills/gitnexus-area-cpp/SKILL.md` |
|
||||
| Work in the Scope-resolution area (72 symbols) | `.claude/skills/gitnexus-area-scope-resolution/SKILL.md` |
|
||||
| Work in the Server area (66 symbols) | `.claude/skills/gitnexus-area-server/SKILL.md` |
|
||||
| Work in the Local area (61 symbols) | `.claude/skills/gitnexus-area-local/SKILL.md` |
|
||||
| Work in the Wiki area (60 symbols) | `.claude/skills/gitnexus-area-wiki/SKILL.md` |
|
||||
| Work in the Workers area (57 symbols) | `.claude/skills/gitnexus-area-workers/SKILL.md` |
|
||||
| Work in the Embeddings area (56 symbols) | `.claude/skills/gitnexus-area-embeddings/SKILL.md` |
|
||||
| Work in the Typescript area (53 symbols) | `.claude/skills/gitnexus-area-typescript/SKILL.md` |
|
||||
| Work in the Storage area (51 symbols) | `.claude/skills/gitnexus-area-storage/SKILL.md` |
|
||||
| Work in the Php area (48 symbols) | `.claude/skills/gitnexus-area-php/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
||||
|
|
|
|||
54
CLAUDE.md
54
CLAUDE.md
|
|
@ -36,7 +36,7 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g
|
|||
|
||||
- **This repository:** [AGENTS.md](AGENTS.md) (Cursor + monorepo notes), [ARCHITECTURE.md](ARCHITECTURE.md), [CONTRIBUTING.md](CONTRIBUTING.md), [GUARDRAILS.md](GUARDRAILS.md).
|
||||
- **Call & inheritance resolution:** See ARCHITECTURE.md § Scope-Resolution Pipeline. Shared pipeline code in `gitnexus/src/core/ingestion/` must not name languages — use `LanguageProvider` / `ScopeResolver` hooks instead (see AGENTS.md). (The legacy call-resolution DAG was removed in #942.)
|
||||
- **GitNexus:** `.claude/skills/gitnexus/`; MCP and indexed-repo rules live only in [AGENTS.md](AGENTS.md) (`gitnexus:start` … `gitnexus:end`). See **GitNexus rules** below.
|
||||
- **GitNexus:** standard skills in `.claude/skills/gitnexus-*/`; MCP and indexed-repo rules live only in [AGENTS.md](AGENTS.md) (`gitnexus:start` … `gitnexus:end`). See **GitNexus rules** below.
|
||||
|
||||
## Changelog
|
||||
|
||||
|
|
@ -88,31 +88,31 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati
|
|||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (239 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
|
||||
| Work in the Extractors area (135 symbols) | `.claude/skills/generated/extractors/SKILL.md` |
|
||||
| Work in the Components area (112 symbols) | `.claude/skills/generated/components/SKILL.md` |
|
||||
| Work in the Lbug area (96 symbols) | `.claude/skills/generated/lbug/SKILL.md` |
|
||||
| Work in the Group area (94 symbols) | `.claude/skills/generated/group/SKILL.md` |
|
||||
| Work in the Cli area (92 symbols) | `.claude/skills/generated/cli/SKILL.md` |
|
||||
| Work in the Configs area (92 symbols) | `.claude/skills/generated/configs/SKILL.md` |
|
||||
| Work in the Type-extractors area (90 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
|
||||
| Work in the Hooks area (88 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
|
||||
| Work in the Unit area (80 symbols) | `.claude/skills/generated/unit/SKILL.md` |
|
||||
| Work in the Cpp area (73 symbols) | `.claude/skills/generated/cpp/SKILL.md` |
|
||||
| Work in the Scope-resolution area (72 symbols) | `.claude/skills/generated/scope-resolution/SKILL.md` |
|
||||
| Work in the Server area (66 symbols) | `.claude/skills/generated/server/SKILL.md` |
|
||||
| Work in the Local area (61 symbols) | `.claude/skills/generated/local/SKILL.md` |
|
||||
| Work in the Wiki area (60 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
|
||||
| Work in the Workers area (57 symbols) | `.claude/skills/generated/workers/SKILL.md` |
|
||||
| Work in the Embeddings area (56 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
|
||||
| Work in the Typescript area (53 symbols) | `.claude/skills/generated/typescript/SKILL.md` |
|
||||
| Work in the Storage area (51 symbols) | `.claude/skills/generated/storage/SKILL.md` |
|
||||
| Work in the Php area (48 symbols) | `.claude/skills/generated/php/SKILL.md` |
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` |
|
||||
| Work in the Ingestion area (239 symbols) | `.claude/skills/gitnexus-area-ingestion/SKILL.md` |
|
||||
| Work in the Extractors area (135 symbols) | `.claude/skills/gitnexus-area-extractors/SKILL.md` |
|
||||
| Work in the Components area (112 symbols) | `.claude/skills/gitnexus-area-components/SKILL.md` |
|
||||
| Work in the Lbug area (96 symbols) | `.claude/skills/gitnexus-area-lbug/SKILL.md` |
|
||||
| Work in the Group area (94 symbols) | `.claude/skills/gitnexus-area-group/SKILL.md` |
|
||||
| Work in the Cli area (92 symbols) | `.claude/skills/gitnexus-area-cli/SKILL.md` |
|
||||
| Work in the Configs area (92 symbols) | `.claude/skills/gitnexus-area-configs/SKILL.md` |
|
||||
| Work in the Type-extractors area (90 symbols) | `.claude/skills/gitnexus-area-type-extractors/SKILL.md` |
|
||||
| Work in the Hooks area (88 symbols) | `.claude/skills/gitnexus-area-hooks/SKILL.md` |
|
||||
| Work in the Unit area (80 symbols) | `.claude/skills/gitnexus-area-unit/SKILL.md` |
|
||||
| Work in the Cpp area (73 symbols) | `.claude/skills/gitnexus-area-cpp/SKILL.md` |
|
||||
| Work in the Scope-resolution area (72 symbols) | `.claude/skills/gitnexus-area-scope-resolution/SKILL.md` |
|
||||
| Work in the Server area (66 symbols) | `.claude/skills/gitnexus-area-server/SKILL.md` |
|
||||
| Work in the Local area (61 symbols) | `.claude/skills/gitnexus-area-local/SKILL.md` |
|
||||
| Work in the Wiki area (60 symbols) | `.claude/skills/gitnexus-area-wiki/SKILL.md` |
|
||||
| Work in the Workers area (57 symbols) | `.claude/skills/gitnexus-area-workers/SKILL.md` |
|
||||
| Work in the Embeddings area (56 symbols) | `.claude/skills/gitnexus-area-embeddings/SKILL.md` |
|
||||
| Work in the Typescript area (53 symbols) | `.claude/skills/gitnexus-area-typescript/SKILL.md` |
|
||||
| Work in the Storage area (51 symbols) | `.claude/skills/gitnexus-area-storage/SKILL.md` |
|
||||
| Work in the Php area (48 symbols) | `.claude/skills/gitnexus-area-php/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ flowchart TB
|
|||
- **Guide** — GitNexus tool/resource/schema reference for the agent
|
||||
- **CLI** — run analyze/status/clean/wiki commands on request
|
||||
|
||||
**Repo-specific skills** — run `gitnexus analyze --skills` and GitNexus detects the functional areas of your codebase (via Leiden community detection) and generates a `SKILL.md` for each one under `.claude/skills/generated/`. Each skill describes a module's key files, entry points, execution flows, and cross-area connections, and is regenerated on each `--skills` run to stay current.
|
||||
**Repo-specific skills** — run `gitnexus analyze --skills` and GitNexus detects the functional areas of your codebase (via Leiden community detection) and generates each one as a direct project skill under `.claude/skills/gitnexus-area-<name>/`. Each skill describes a module's key files, entry points, execution flows, and cross-area connections, and is regenerated on each `--skills` run to stay current.
|
||||
|
||||
## Editor Setup
|
||||
|
||||
|
|
@ -347,7 +347,7 @@ gitnexus analyze --skills # Generate repo-specific skill files from detec
|
|||
gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
|
||||
gitnexus analyze --embeddings [limit] # Enable embedding generation (slower, better search)
|
||||
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
|
||||
gitnexus analyze --skip-skills # Skip installing .claude/skills/gitnexus/ skill files
|
||||
gitnexus analyze --skip-skills # Skip installing standard .claude/skills/gitnexus-* skill files
|
||||
gitnexus analyze --skip-git # Index folders that are not Git repositories
|
||||
gitnexus analyze --default-branch develop # Branch used in the generated regression-compare example (base_ref)
|
||||
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
|
||||
|
|
@ -403,7 +403,7 @@ Commit a `.gitnexusrc` JSON file at the repo root to preconfigure recurring `ana
|
|||
// over its fix on every analyze. (Alias: "branch".)
|
||||
"defaultBranch": "develop",
|
||||
"skipContextFiles": true, // alias of skipAgentsMd: keep your own AGENTS.md/CLAUDE.md
|
||||
"skipSkills": true, // don't install .claude/skills/gitnexus/
|
||||
"skipSkills": true, // don't install standard .claude/skills/gitnexus-* skills
|
||||
"embeddings": true, // generate embeddings by default
|
||||
"workerTimeout": 60
|
||||
}
|
||||
|
|
|
|||
207
docs/plans/2026-07-11-001-fix-claude-skill-discovery-plan.md
Normal file
207
docs/plans/2026-07-11-001-fix-claude-skill-discovery-plan.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
title: Claude Skill Discovery Paths - Plan
|
||||
type: fix
|
||||
date: 2026-07-11
|
||||
artifact_contract: ce-unified-plan/v1
|
||||
artifact_readiness: implementation-ready
|
||||
product_contract_source: ce-plan-bootstrap
|
||||
execution: code
|
||||
---
|
||||
|
||||
# Claude Skill Discovery Paths - Plan
|
||||
|
||||
## Goal Capsule
|
||||
|
||||
- **Objective:** Make every Claude Code skill written by `gitnexus analyze` discoverable from the project skill root while preserving skip flags, repeat-run stability, and unrelated user skills.
|
||||
- **Authority:** GitHub issue #2433 and Claude Code's documented project-skill layout are the behavioral contract; repository guardrails and existing CLI conventions govern implementation.
|
||||
- **Execution profile:** Standard, test-first bug fix in `gitnexus/`; no dependency, schema, or public MCP changes.
|
||||
- **Stop conditions:** Stop if the fix requires deleting unrecognized user-owned skill directories, changes `--skip-skills` semantics, or impact analysis reports HIGH/CRITICAL risk without maintainer approval.
|
||||
- **Tail ownership:** LFG owns simplification, review, commits, PR creation, and CI follow-through after the implementation units pass verification.
|
||||
|
||||
---
|
||||
|
||||
## Product Contract
|
||||
|
||||
### Summary
|
||||
|
||||
Install standard and repo-generated Claude Code skills as direct children of `.claude/skills/`, update all generated references and CLI messages to those paths, and migrate known legacy GitNexus outputs without touching unrelated project skills.
|
||||
|
||||
### Problem Frame
|
||||
|
||||
`gitnexus analyze` currently writes standard skills below `.claude/skills/gitnexus/` and community skills below `.claude/skills/generated/`.
|
||||
Claude Code treats `.claude/skills/<skill-name>/SKILL.md` as the project-skill shape; nested `.claude/skills/` directories elsewhere in a monorepo are separate discovery roots, not grouping directories inside a skill root.
|
||||
The current installer therefore reports success and writes managed instructions that point to files, but the skills are not registered for invocation.
|
||||
|
||||
### Requirements
|
||||
|
||||
**Standard skills**
|
||||
|
||||
- R1. Each bundled `gitnexus-*` standard skill is written to `.claude/skills/<skill-name>/SKILL.md`.
|
||||
- R2. Generated AGENTS.md and CLAUDE.md routing rows reference the same direct standard-skill paths.
|
||||
|
||||
**Community skills**
|
||||
|
||||
- R3. Each `--skills` community skill is written directly below `.claude/skills/` with a GitNexus-owned name that cannot collide with the six standard skills or ordinary unprefixed project skills.
|
||||
- R4. Community skill frontmatter, returned metadata, console output, and generated routing rows use one consistent discoverable name and path.
|
||||
|
||||
**Migration and compatibility**
|
||||
|
||||
- R5. A repeat analyze removes or replaces only legacy directories GitNexus can identify as its own output and preserves unrelated `.claude/skills/` entries.
|
||||
- R6. `--skip-skills` continues to suppress only the six standard skills, while `--skills` community generation remains independent; `--index-only` continues to suppress all context-file injection.
|
||||
- R7. CLI help and localized help text describe the corrected paths without changing flag behavior.
|
||||
- R8. This repository's checked-in copies of the six standard skills and its managed AGENTS.md/CLAUDE.md routing rows use the corrected direct layout when the fix lands.
|
||||
|
||||
### Acceptance Examples
|
||||
|
||||
- AE1. Given a clean repository, a normal analyze creates `.claude/skills/gitnexus-exploring/SKILL.md`, does not create `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md`, and emits the direct path in AGENTS.md and CLAUDE.md.
|
||||
- AE2. Given `analyze --skills`, each generated community skill has a direct, namespaced directory below `.claude/skills/`, and the context-file table points to that exact file.
|
||||
- AE3. Given existing unrelated project skills plus legacy GitNexus grouping directories, rerunning analyze preserves the unrelated skills, produces the direct GitNexus skills, and leaves no managed reference pointing at a legacy grouped path.
|
||||
- AE4. Given `--skip-skills`, no standard `gitnexus-*` skill is installed or referenced, while generated community skill behavior remains available when `--skills` is also requested.
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- All standard and generated skill files use Claude Code's documented direct-child layout.
|
||||
- Generated context, return messages, help text, and tests contain no active references to `.claude/skills/gitnexus/` or `.claude/skills/generated/`.
|
||||
- The canonical repository no longer ships the six standard skills or managed routing rows in the broken grouped layout.
|
||||
- Repeated runs are deterministic and do not delete unrelated user skills.
|
||||
|
||||
### Scope Boundaries
|
||||
|
||||
- **In scope:** project-local Claude Code skill installation performed by `analyze`, repo-generated community skills, managed context paths, the repository's checked-in copies of the six standard skills, CLI/help copy, migration of known legacy outputs, and regression coverage.
|
||||
- **Out of scope:** global `gitnexus setup` targets, plugin skill layouts, changing the six bundled skill bodies, or changing Claude Code itself.
|
||||
- **Deferred to follow-up work:** relocating this repository's three extra hand-maintained nested `.claude/skills/gitnexus/` skills that are not installed by `analyze`; those are workspace configuration rather than the issue's six standard installer outputs.
|
||||
|
||||
### Sources
|
||||
|
||||
- GitHub issue #2433: `https://github.com/abhigyanpatwari/GitNexus/issues/2433`
|
||||
- Claude Code skills documentation: `https://code.claude.com/docs/en/slash-commands`
|
||||
- Related path-contract regressions: GitHub issues #1098 and #1381.
|
||||
|
||||
---
|
||||
|
||||
## Planning Contract
|
||||
|
||||
### Key Technical Decisions
|
||||
|
||||
- KTD1. Treat `.claude/skills/` as the installation root and make each skill directory its direct child. This matches the official project-skill contract and avoids relying on recursive discovery that Claude Code does not document.
|
||||
- KTD2. Keep the six standard names unchanged because they are already `gitnexus-*` namespaced. This preserves their intended invocation names while correcting only the filesystem layout.
|
||||
- KTD3. Reserve a separate GitNexus-owned prefix for generated community skill names before writing them flat. This prevents a community such as `Cli` from overwriting `gitnexus-cli` and prevents common labels such as `auth` from replacing user skills.
|
||||
- KTD4. Replace grouped-directory cleanup with ownership-bounded cleanup. Remove known standard legacy children and generated legacy output, or direct generated directories carrying the reserved prefix, but never recursively clear `.claude/skills/` itself.
|
||||
- KTD5. Keep generation and documentation derived from the same `GeneratedSkillInfo.name` value so disk paths, frontmatter names, managed routing rows, and repeat-run cleanup cannot drift.
|
||||
|
||||
### Assumptions
|
||||
|
||||
- Issue #2433's request to check community skills includes fixing them in this PR rather than filing a separate follow-up.
|
||||
- Legacy `.claude/skills/generated/` is GitNexus-owned because current code already deletes and recreates it on every `--skills` run; unknown siblings under `.claude/skills/` remain user-owned.
|
||||
- Standard legacy cleanup is limited to the six bundled names under `.claude/skills/gitnexus/`; unknown children in that grouping directory are preserved.
|
||||
- The exact generated-skill prefix may be refined during implementation, but it must be stable, GitNexus-owned, direct-child compatible, and non-conflicting with standard skill names.
|
||||
|
||||
### Existing Patterns to Follow
|
||||
|
||||
- `gitnexus/src/cli/setup.ts` installs globally scoped Claude skills directly under the target skill root and provides a path-contract precedent.
|
||||
- `gitnexus/src/cli/ai-context.ts` already centralizes standard skill definitions, context table generation, skip semantics, and best-effort filesystem handling.
|
||||
- `gitnexus/src/cli/skill-gen.ts` already owns community-name normalization, deterministic collision suffixes, output cleanup, frontmatter rendering, and returned path metadata.
|
||||
- `gitnexus/test/unit/ai-context.test.ts` uses temporary repositories to prove file layout and skip-mode behavior.
|
||||
- `gitnexus/test/unit/skill-gen.test.ts` and `gitnexus/test/integration/skills-e2e.test.ts` cover generated skill metadata, file contents, idempotency, and end-to-end context references.
|
||||
|
||||
### System-Wide Impact
|
||||
|
||||
The change affects the user-visible filesystem contract of `gitnexus analyze`, generated AGENTS.md/CLAUDE.md content, CLI help output, and the invocation names of previously inert community skills.
|
||||
It does not alter indexing, graph storage, MCP APIs, global setup targets, or runtime analysis behavior.
|
||||
|
||||
### Risks and Mitigations
|
||||
|
||||
- **Accidental user-skill deletion:** Scope cleanup to known standard names, the prior generated output directory, and the new reserved prefix; add preservation tests with unrelated directories.
|
||||
- **Standard/community collision:** Use distinct namespaces and assert representative `Cli`/common-label cases.
|
||||
- **Path drift across surfaces:** Derive context rows from returned generated names and assert exact disk-to-doc parity.
|
||||
- **Skip-mode regression:** Retain focused tests for normal, `--skip-skills`, `--skills`, and `--index-only` combinations.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Units
|
||||
|
||||
### U1. Flatten standard skill installation and managed references
|
||||
|
||||
- **Goal:** Install the six bundled skills as direct project skills and migrate only their known legacy copies.
|
||||
- **Requirements:** R1, R2, R5, R6; AE1, AE3, AE4.
|
||||
- **Dependencies:** None.
|
||||
- **Files:** `gitnexus/src/cli/ai-context.ts`, `gitnexus/test/unit/ai-context.test.ts`.
|
||||
- **Approach:** Change the standard install root and routing-table templates together; preserve `skipSkills` behavior and result reporting; add bounded cleanup for the six known legacy child directories while preserving unknown siblings and unrelated direct skills.
|
||||
- **Execution note:** Start with failing temporary-repository assertions for the direct path, absence of the legacy path, preservation of unrelated skills, and repeated-run behavior.
|
||||
- **Patterns to follow:** Existing `installSkills`, `generateGitNexusContent`, and temporary-directory tests in `ai-context.test.ts`.
|
||||
- **Test scenarios:**
|
||||
- Covers AE1. A default run writes all six direct skill files and emits the same direct paths in both context files.
|
||||
- Covers AE3. A run with an unrelated direct skill and an unknown legacy-group child preserves both while replacing known legacy standard children.
|
||||
- Covers AE4. `skipSkills` writes no standard direct skill, emits no standard routing row, and reports the corrected skipped location.
|
||||
- A second default run produces the same six skills without duplicate directories or context rows.
|
||||
- **Verification:** Focused AI-context tests prove the filesystem, managed-document, migration, and skip contracts.
|
||||
|
||||
### U2. Flatten and namespace generated community skills
|
||||
|
||||
- **Goal:** Make `--skills` outputs discoverable without colliding with standard or user-authored skills.
|
||||
- **Requirements:** R3, R4, R5, R6; AE2, AE3, AE4.
|
||||
- **Dependencies:** U1 establishes the shared direct-root convention.
|
||||
- **Files:** `gitnexus/src/cli/skill-gen.ts`, `gitnexus/test/unit/skill-gen.test.ts`, `gitnexus/test/integration/skills-e2e.test.ts`, `gitnexus/test/unit/analyze-no-stats-bridge.test.ts`, `gitnexus/test/unit/analyze-gitnexusrc.test.ts`.
|
||||
- **Approach:** Generate reserved, deterministic community names; write each directory directly under `.claude/skills/`; clean only legacy generated output and stale directories in the reserved namespace; return and render the direct path consistently; update mocked path fixtures that model the output contract.
|
||||
- **Execution note:** Characterize existing name normalization and idempotency first, then add red tests for a community label that would collide with a standard or common user skill.
|
||||
- **Patterns to follow:** `toKebabName`, `renderSkillMarkdown`, and existing repeat-run tests.
|
||||
- **Test scenarios:**
|
||||
- Covers AE2. A representative community produces a direct namespaced directory whose basename equals frontmatter `name` and returned metadata `name`.
|
||||
- A `Cli` community does not overwrite the standard `gitnexus-cli` skill.
|
||||
- A pre-existing unrelated `.claude/skills/auth/SKILL.md` survives generation of an Auth community.
|
||||
- Covers AE3. A repeat run removes stale GitNexus-generated community directories and the legacy `generated/` output while preserving unrelated direct skills.
|
||||
- The end-to-end `analyze --skills` fixture finds generated files at direct paths and context tables point to those exact paths on first and second runs.
|
||||
- **Verification:** Unit and integration tests prove collision resistance, ownership-bounded cleanup, path/frontmatter parity, and deterministic regeneration.
|
||||
|
||||
### U3. Align CLI help and path-contract assertions
|
||||
|
||||
- **Goal:** Remove stale user-facing descriptions of grouped skill directories and lock the corrected contract into CLI coverage.
|
||||
- **Requirements:** R7 and the active-reference portion of R2/R4.
|
||||
- **Dependencies:** U1 and U2 determine the final standard and generated naming conventions.
|
||||
- **Files:** `gitnexus/src/cli/index.ts`, `gitnexus/src/cli/i18n/zh-CN.ts`, `gitnexus/test/unit/skip-git-cli.test.ts`, `gitnexus/test/unit/ai-context.test.ts`, `gitnexus/test/integration/skills-e2e.test.ts`.
|
||||
- **Approach:** Update English and Chinese help copy and strengthen existing help/context assertions so legacy grouped paths fail tests if reintroduced.
|
||||
- **Patterns to follow:** Existing Commander option descriptions, `help.option.analyze.*` translation keys, and `skip-git-cli.test.ts` help assertions.
|
||||
- **Test scenarios:**
|
||||
- `gitnexus analyze --help` names the direct standard location and the reserved direct community naming convention.
|
||||
- Generated AGENTS.md and CLAUDE.md contain no active `.claude/skills/gitnexus/` or `.claude/skills/generated/` routing entries.
|
||||
- Chinese help retains the same flag semantics while naming corrected locations.
|
||||
- **Verification:** Focused CLI/help tests and repository search confirm stale active path copy is gone from changed runtime and test surfaces.
|
||||
|
||||
### U4. Align the repository's checked-in standard skills
|
||||
|
||||
- **Goal:** Ensure the canonical GitNexus checkout demonstrates the same discoverable layout the corrected analyzer produces.
|
||||
- **Requirements:** R8 and the repository-facing portion of R2.
|
||||
- **Dependencies:** U1 establishes the standard direct paths.
|
||||
- **Files:** `.claude/skills/gitnexus-exploring/SKILL.md`, `.claude/skills/gitnexus-debugging/SKILL.md`, `.claude/skills/gitnexus-impact-analysis/SKILL.md`, `.claude/skills/gitnexus-refactoring/SKILL.md`, `.claude/skills/gitnexus-guide/SKILL.md`, `.claude/skills/gitnexus-cli/SKILL.md`, `AGENTS.md`, `CLAUDE.md`.
|
||||
- **Approach:** Relocate exactly the six analyzer-installed standard skill directories from the grouped path to direct children and update only their managed routing rows; preserve the extra hand-maintained nested skills unchanged.
|
||||
- **Patterns to follow:** The direct paths produced by U1 and the existing GitNexus-managed block markers in AGENTS.md and CLAUDE.md.
|
||||
- **Test scenarios:** Test expectation: none -- this unit relocates checked-in skill assets without changing their bodies; repository search and the focused path-contract tests cover their discoverability contract.
|
||||
- **Verification:** Each of the six direct files exists with unchanged content, the six legacy grouped copies are absent, the three extra nested skill directories remain, and both managed tables point to the direct files.
|
||||
|
||||
---
|
||||
|
||||
## Verification Contract
|
||||
|
||||
| Gate | Command | Proves |
|
||||
|---|---|---|
|
||||
| Focused standard installer | `cd gitnexus && npx vitest run test/unit/ai-context.test.ts` | Direct standard paths, managed rows, migration safety, skip flags |
|
||||
| Focused community generator | `cd gitnexus && npx vitest run test/unit/skill-gen.test.ts` | Namespacing, collision handling, cleanup, metadata/frontmatter parity |
|
||||
| CLI help | `cd gitnexus && npx vitest run test/unit/skip-git-cli.test.ts` | User-facing flag path contract |
|
||||
| Community end to end | `cd gitnexus && npx vitest run test/integration/skills-e2e.test.ts` | Real analyze output and repeat-run references across fixtures |
|
||||
| CLI/Core regression | `cd gitnexus && npm test` | Full package behavior |
|
||||
| Type safety | `cd gitnexus && npx tsc --noEmit` | TypeScript contract integrity |
|
||||
| Change scope | GitNexus `detect_changes` before each commit | Only expected CLI skill-generation symbols and flows are affected |
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- U1-U3 requirements and test scenarios pass.
|
||||
- U4's checked-in relocation and managed-row verification pass.
|
||||
- Standard and community skills are direct children of `.claude/skills/` and discoverable by documented Claude Code rules.
|
||||
- No runtime or generated-document surface points to the two legacy grouping layouts.
|
||||
- Unrelated user-authored skills and unknown legacy-group children are preserved by regression tests.
|
||||
- `--skip-skills`, `--skills`, and `--index-only` retain their documented independence.
|
||||
- Full `gitnexus` tests and typecheck pass, or any environment-only exception is documented with focused proof.
|
||||
- GitNexus change detection reports only the expected CLI generation and test scope.
|
||||
- Abandoned experimental code and temporary artifacts from implementation are absent from the final diff.
|
||||
|
|
@ -233,7 +233,7 @@ gitnexus analyze --embeddings # Enable embedding generation (slower, better s
|
|||
gitnexus embeddings install # Fetch the optional local embedding stack on demand (--cuda, --force)
|
||||
gitnexus analyze --skills # Generate repo-specific skill files from detected communities
|
||||
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
|
||||
gitnexus analyze --skip-skills # Skip installing .claude/skills/gitnexus/ skill files
|
||||
gitnexus analyze --skip-skills # Skip installing standard .claude/skills/gitnexus-* skill files
|
||||
gitnexus analyze --skip-git # Index folders that are not Git repositories
|
||||
gitnexus analyze --workers <n> # Parse worker pool size (>=1; default: cores-1, capped at 16)
|
||||
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
|
||||
|
|
@ -328,7 +328,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
|
|||
- **Guide** — GitNexus tool/resource/schema reference for the agent
|
||||
- **CLI** — Run analyze/status/clean/wiki commands on request
|
||||
|
||||
Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setup` (global). Run `gitnexus analyze --skills` to additionally generate repo-specific skills for each detected functional area under `.claude/skills/generated/`.
|
||||
Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setup` (global). Run `gitnexus analyze --skills` to additionally generate each detected functional area as a direct project skill under `.claude/skills/gitnexus-area-<name>/`.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export function generateGitNexusContent(
|
|||
? generatedSkills
|
||||
.map(
|
||||
(s) =>
|
||||
`| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`,
|
||||
`| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/${s.name}/SKILL.md\` |`,
|
||||
)
|
||||
.join('\n')
|
||||
: '';
|
||||
|
|
@ -163,16 +163,16 @@ export function generateGitNexusContent(
|
|||
// Standard skill rows reference files installed by installSkills(). When
|
||||
// --skip-skills suppresses that install, these rows must be omitted — else
|
||||
// AGENTS.md/CLAUDE.md would direct agents to read files that don't exist.
|
||||
// Community skills (generatedRows) live in .claude/skills/generated/ and
|
||||
// Community skills (generatedRows) live directly under .claude/skills/ and
|
||||
// are independent of --skip-skills, so they remain when present.
|
||||
const standardSkillsRows = skipSkills
|
||||
? ''
|
||||
: `| Understand architecture / "How does X work?" | \`.claude/skills/gitnexus/gitnexus-exploring/SKILL.md\` |
|
||||
| Blast radius / "What breaks if I change X?" | \`.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md\` |
|
||||
| Trace bugs / "Why is X failing?" | \`.claude/skills/gitnexus/gitnexus-debugging/SKILL.md\` |
|
||||
| Rename / extract / split / refactor | \`.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md\` |
|
||||
| Tools, resources, schema reference | \`.claude/skills/gitnexus/gitnexus-guide/SKILL.md\` |
|
||||
| Index, status, clean, wiki CLI commands | \`.claude/skills/gitnexus/gitnexus-cli/SKILL.md\` |`;
|
||||
: `| Understand architecture / "How does X work?" | \`.claude/skills/gitnexus-exploring/SKILL.md\` |
|
||||
| Blast radius / "What breaks if I change X?" | \`.claude/skills/gitnexus-impact-analysis/SKILL.md\` |
|
||||
| Trace bugs / "Why is X failing?" | \`.claude/skills/gitnexus-debugging/SKILL.md\` |
|
||||
| Rename / extract / split / refactor | \`.claude/skills/gitnexus-refactoring/SKILL.md\` |
|
||||
| Tools, resources, schema reference | \`.claude/skills/gitnexus-guide/SKILL.md\` |
|
||||
| Index, status, clean, wiki CLI commands | \`.claude/skills/gitnexus-cli/SKILL.md\` |`;
|
||||
|
||||
const tableBody = [standardSkillsRows, generatedRows].filter(Boolean).join('\n');
|
||||
const skillsTable = tableBody
|
||||
|
|
@ -364,11 +364,12 @@ async function upsertGitNexusSection(
|
|||
}
|
||||
|
||||
/**
|
||||
* Install GitNexus skills to .claude/skills/gitnexus/
|
||||
* Install GitNexus skills as direct children of .claude/skills/
|
||||
* Works natively with Claude Code, Cursor, and GitHub Copilot
|
||||
*/
|
||||
async function installSkills(repoPath: string): Promise<string[]> {
|
||||
const skillsDir = path.join(repoPath, '.claude', 'skills', 'gitnexus');
|
||||
const skillsDir = path.join(repoPath, '.claude', 'skills');
|
||||
const legacySkillsDir = path.join(skillsDir, 'gitnexus');
|
||||
const installedSkills: string[] = [];
|
||||
|
||||
// Skill definitions bundled with the package
|
||||
|
|
@ -436,6 +437,15 @@ Use GitNexus tools to accomplish this task.
|
|||
|
||||
await fs.writeFile(skillPath, skillContent, 'utf-8');
|
||||
installedSkills.push(skill.name);
|
||||
|
||||
// Previous releases installed these known standard skills one level too
|
||||
// deep. Remove only the child owned by this installer; unknown siblings
|
||||
// under the legacy grouping directory may be user-authored and survive.
|
||||
try {
|
||||
await fs.rm(path.join(legacySkillsDir, skill.name), { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
logger.warn({ err }, `Warning: Could not remove legacy skill ${skill.name}:`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Skip on error, don't fail the whole process
|
||||
logger.warn({ err }, `Warning: Could not install skill ${skill.name}:`);
|
||||
|
|
@ -518,14 +528,14 @@ export async function generateAIContextFiles(
|
|||
createdFiles.push('CLAUDE.md (skipped via --skip-agents-md)');
|
||||
}
|
||||
|
||||
// Install skills to .claude/skills/gitnexus/ (unless --skip-skills)
|
||||
// Install standard skills directly under .claude/skills/ (unless --skip-skills)
|
||||
if (!options?.skipSkills) {
|
||||
const installedSkills = await installSkills(repoPath);
|
||||
if (installedSkills.length > 0) {
|
||||
createdFiles.push(`.claude/skills/gitnexus/ (${installedSkills.length} skills)`);
|
||||
createdFiles.push(`.claude/skills/gitnexus-*/ (${installedSkills.length} skills)`);
|
||||
}
|
||||
} else {
|
||||
createdFiles.push('.claude/skills/gitnexus/ (skipped via --skip-skills)');
|
||||
createdFiles.push('.claude/skills/gitnexus-*/ (skipped via --skip-skills)');
|
||||
}
|
||||
|
||||
return { files: createdFiles };
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@ export interface AnalyzeOptions {
|
|||
* default-on case.
|
||||
*/
|
||||
stats?: boolean;
|
||||
/** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */
|
||||
/** Skip installing standard GitNexus skill files directly under .claude/skills/. */
|
||||
skipSkills?: boolean;
|
||||
/**
|
||||
* Default branch for the generated regression-compare example (#243). From
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ export const en = {
|
|||
'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md',
|
||||
'help.option.analyze.noStats': 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md',
|
||||
'help.option.analyze.skipSkills':
|
||||
'Skip installing standard GitNexus skill files under .claude/skills/gitnexus/. Does not suppress community skills from --skills (those use .claude/skills/generated/). Use --index-only to skip all AI-context file injection.',
|
||||
'Skip installing standard GitNexus skill files directly under .claude/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.',
|
||||
'help.option.analyze.indexOnly':
|
||||
'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)',
|
||||
'help.option.skipGit':
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export const zhCN = {
|
|||
'help.option.analyze.skipAgentsMd': '跳过更新 AGENTS.md 和 CLAUDE.md 中的 gitnexus 区块',
|
||||
'help.option.analyze.noStats': '从 AGENTS.md 和 CLAUDE.md 中省略易变的文件/符号计数',
|
||||
'help.option.analyze.skipSkills':
|
||||
'跳过安装 .claude/skills/gitnexus/ 下的标准 GitNexus skill 文件。不抑制 --skills 生成的社区 skill(位于 .claude/skills/generated/)。使用 --index-only 可跳过所有 AI 上下文文件注入。',
|
||||
'跳过直接安装在 .claude/skills/ 下的标准 GitNexus skill 文件。不抑制 --skills 生成的社区 skill(位于 .claude/skills/gitnexus-area-*)。使用 --index-only 可跳过所有 AI 上下文文件注入。',
|
||||
'help.option.analyze.indexOnly': '纯索引模式:跳过所有文件注入(AGENTS.md、CLAUDE.md、skills)',
|
||||
'help.option.skipGit': '将提供的路径/cwd 视为索引根目录,并跳过向上查找 git 根目录',
|
||||
'help.option.analyze.name':
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ program
|
|||
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
|
||||
.option(
|
||||
'--skip-skills',
|
||||
'Skip installing standard GitNexus skill files under .claude/skills/gitnexus/. ' +
|
||||
'Does not suppress community skills from --skills (those use .claude/skills/generated/). ' +
|
||||
'Skip installing standard GitNexus skill files directly under .claude/skills/. ' +
|
||||
'Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). ' +
|
||||
'Use --index-only to skip all AI-context file injection.',
|
||||
)
|
||||
.option('--index-only', 'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)')
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import { CommunityNode, CommunityMembership } from '../core/ingestion/community-
|
|||
import { ProcessNode } from '../core/ingestion/process-processor.js';
|
||||
import { KnowledgeGraph } from '../core/graph/types.js';
|
||||
|
||||
const GENERATED_SKILL_PREFIX = 'gitnexus-area-';
|
||||
const MAX_SKILL_NAME_LENGTH = 64;
|
||||
const MAX_COMMUNITY_NAME_LENGTH = MAX_SKILL_NAME_LENGTH - GENERATED_SKILL_PREFIX.length;
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
|
@ -68,7 +72,28 @@ export const generateSkillFiles = async (
|
|||
pipelineResult: PipelineResult,
|
||||
): Promise<{ skills: GeneratedSkillInfo[]; outputPath: string }> => {
|
||||
const { communityResult, processResult, graph } = pipelineResult;
|
||||
const outputDir = path.join(repoPath, '.claude', 'skills', 'generated');
|
||||
const outputDir = path.join(repoPath, '.claude', 'skills');
|
||||
const legacyOutputDir = path.join(outputDir, 'generated');
|
||||
|
||||
// Community skills used to live under an undiscoverable `generated/`
|
||||
// grouping directory. Clear that GitNexus-owned legacy output and
|
||||
// stale direct outputs in the reserved namespace, while preserving every
|
||||
// unrelated project skill under .claude/skills/.
|
||||
try {
|
||||
const entries = await fs.readdir(outputDir, { withFileTypes: true });
|
||||
await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith(GENERATED_SKILL_PREFIX))
|
||||
.map((entry) => fs.rm(path.join(outputDir, entry.name), { recursive: true, force: true })),
|
||||
);
|
||||
} catch {
|
||||
/* output root may not exist yet */
|
||||
}
|
||||
try {
|
||||
await fs.rm(legacyOutputDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* legacy output may not exist */
|
||||
}
|
||||
|
||||
if (!communityResult || !communityResult.memberships.length) {
|
||||
console.log('\n Skills: no communities detected, skipping skill generation');
|
||||
|
|
@ -107,12 +132,8 @@ export const generateSkillFiles = async (
|
|||
communities,
|
||||
);
|
||||
|
||||
// Step 4: Clear and recreate output directory
|
||||
try {
|
||||
await fs.rm(outputDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* may not exist */
|
||||
}
|
||||
// Step 4: Ensure the shared project-skill root exists. Never clear it: it
|
||||
// also contains user-authored and standard GitNexus skills.
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
// Step 5: Generate skill files
|
||||
|
|
@ -145,6 +166,7 @@ export const generateSkillFiles = async (
|
|||
// Generate kebab name
|
||||
const kebabName = toKebabName(community.label, usedNames);
|
||||
usedNames.add(kebabName);
|
||||
const skillName = `${GENERATED_SKILL_PREFIX}${kebabName}`;
|
||||
|
||||
// Generate SKILL.md content
|
||||
const content = renderSkillMarkdown(
|
||||
|
|
@ -155,16 +177,16 @@ export const generateSkillFiles = async (
|
|||
entryPoints,
|
||||
flows,
|
||||
connections,
|
||||
kebabName,
|
||||
skillName,
|
||||
);
|
||||
|
||||
// Write file
|
||||
const skillDir = path.join(outputDir, kebabName);
|
||||
const skillDir = path.join(outputDir, skillName);
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
|
||||
|
||||
const info: GeneratedSkillInfo = {
|
||||
name: kebabName,
|
||||
name: skillName,
|
||||
label: community.label,
|
||||
symbolCount: community.symbolCount,
|
||||
fileCount: files.length,
|
||||
|
|
@ -176,7 +198,9 @@ export const generateSkillFiles = async (
|
|||
);
|
||||
}
|
||||
|
||||
console.log(`\n ${skills.length} skills generated \u2192 .claude/skills/generated/`);
|
||||
console.log(
|
||||
`\n ${skills.length} skills generated \u2192 .claude/skills/${GENERATED_SKILL_PREFIX}*/`,
|
||||
);
|
||||
|
||||
return { skills, outputPath: outputDir };
|
||||
};
|
||||
|
|
@ -522,7 +546,7 @@ const gatherCrossConnections = (
|
|||
* @param {MemberSymbol[]} entryPoints - Exported entry point symbols
|
||||
* @param {ProcessNode[]} flows - Execution flows touching this community
|
||||
* @param {CrossConnection[]} connections - Cross-community connections
|
||||
* @param {string} kebabName - Kebab-case name for the skill
|
||||
* @param {string} skillName - Namespaced kebab-case name for the skill
|
||||
* @returns {string} Full SKILL.md content
|
||||
*/
|
||||
const renderSkillMarkdown = (
|
||||
|
|
@ -533,7 +557,7 @@ const renderSkillMarkdown = (
|
|||
entryPoints: MemberSymbol[],
|
||||
flows: ProcessNode[],
|
||||
connections: CrossConnection[],
|
||||
kebabName: string,
|
||||
skillName: string,
|
||||
): string => {
|
||||
const cohesionPct = Math.round(community.cohesion * 100);
|
||||
|
||||
|
|
@ -551,7 +575,7 @@ const renderSkillMarkdown = (
|
|||
|
||||
// Frontmatter
|
||||
lines.push('---');
|
||||
lines.push(`name: ${kebabName}`);
|
||||
lines.push(`name: ${skillName}`);
|
||||
lines.push(
|
||||
`description: "Skill for the ${community.label} area of ${projectName}. ${community.symbolCount} symbols across ${files.length} files."`,
|
||||
);
|
||||
|
|
@ -670,21 +694,22 @@ const renderSkillMarkdown = (
|
|||
* @brief Convert a community label to a kebab-case directory name
|
||||
* @param {string} label - The community label
|
||||
* @param {Set<string>} usedNames - Already-used names for collision detection
|
||||
* @returns {string} Unique kebab-case name capped at 50 characters
|
||||
* @returns {string} Unique kebab-case name that leaves room for the GitNexus prefix
|
||||
*/
|
||||
const toKebabName = (label: string, usedNames: Set<string>): string => {
|
||||
let name = label
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 50);
|
||||
.slice(0, MAX_COMMUNITY_NAME_LENGTH);
|
||||
|
||||
if (!name) name = 'skill';
|
||||
|
||||
let candidate = name;
|
||||
let counter = 2;
|
||||
while (usedNames.has(candidate)) {
|
||||
candidate = `${name}-${counter}`;
|
||||
const suffix = `-${counter}`;
|
||||
candidate = `${name.slice(0, MAX_COMMUNITY_NAME_LENGTH - suffix.length)}${suffix}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ export interface AnalyzeOptions {
|
|||
skipAgentsMd?: boolean;
|
||||
/** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
|
||||
noStats?: boolean;
|
||||
/** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */
|
||||
/** Skip installing standard GitNexus skill files directly under .claude/skills/. */
|
||||
skipSkills?: boolean;
|
||||
/**
|
||||
* Build the CFG/PDG substrate (#2081 M1). Forwarded to `PipelineOptions.pdg`,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function createFixtureRepo(prefix: string, files: Record<string, string>): strin
|
|||
* Assert standard skill file properties:
|
||||
* 1. CLI exits 0
|
||||
* 2. .gitnexus/ exists
|
||||
* 3. >= minSkills SKILL.md files under .claude/skills/generated/
|
||||
* 3. >= minSkills direct SKILL.md files under .claude/skills/gitnexus-area-<name>/
|
||||
* 4. YAML frontmatter valid
|
||||
* 5. ## Key Files section present
|
||||
* 6. ## How to Explore section present
|
||||
|
|
@ -99,8 +99,8 @@ function assertSkillFiles(
|
|||
|
||||
expect(fs.existsSync(path.join(tmpDir, '.gitnexus'))).toBe(true);
|
||||
|
||||
const generatedDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
||||
if (!fs.existsSync(generatedDir)) {
|
||||
const skillsRoot = path.join(tmpDir, '.claude', 'skills');
|
||||
if (!fs.existsSync(skillsRoot)) {
|
||||
// Native parser may have crashed in worker or Leiden produced 0 communities.
|
||||
// The pipeline still succeeds (exit 0) but no skills are generated.
|
||||
// Skip skill assertions gracefully — this is platform-dependent.
|
||||
|
|
@ -108,11 +108,19 @@ function assertSkillFiles(
|
|||
}
|
||||
|
||||
const skillDirs = fs
|
||||
.readdirSync(generatedDir)
|
||||
.filter((d) => fs.statSync(path.join(generatedDir, d)).isDirectory());
|
||||
.readdirSync(skillsRoot)
|
||||
.filter(
|
||||
(d) => d.startsWith('gitnexus-area-') && fs.statSync(path.join(skillsRoot, d)).isDirectory(),
|
||||
);
|
||||
if (skillDirs.length === 0) {
|
||||
// Native parser may have crashed in worker or Leiden produced 0 communities.
|
||||
// Standard skills still create the shared root, so absence is now detected
|
||||
// by the reserved community-skill prefix rather than by the root directory.
|
||||
return false;
|
||||
}
|
||||
const skillFiles: string[] = [];
|
||||
for (const dir of skillDirs) {
|
||||
const skillPath = path.join(generatedDir, dir, 'SKILL.md');
|
||||
const skillPath = path.join(skillsRoot, dir, 'SKILL.md');
|
||||
if (fs.existsSync(skillPath)) {
|
||||
skillFiles.push(skillPath);
|
||||
}
|
||||
|
|
@ -135,27 +143,29 @@ function assertSkillFiles(
|
|||
|
||||
/**
|
||||
* Assert CLAUDE.md and AGENTS.md contain generated skill references.
|
||||
* Automatically detects whether skills were generated by checking for
|
||||
* the generated/ directory.
|
||||
* Automatically detects whether community skills were generated by checking
|
||||
* for the reserved direct-child namespace.
|
||||
*/
|
||||
function assertContextFiles(result: ReturnType<typeof runSkillsCli>, tmpDir: string) {
|
||||
if (result.status === null) return;
|
||||
|
||||
const generatedDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
||||
const skillsGenerated = fs.existsSync(generatedDir);
|
||||
const skillsRoot = path.join(tmpDir, '.claude', 'skills');
|
||||
const skillsGenerated =
|
||||
fs.existsSync(skillsRoot) &&
|
||||
fs.readdirSync(skillsRoot).some((entry) => entry.startsWith('gitnexus-area-'));
|
||||
|
||||
const claudePath = path.join(tmpDir, 'CLAUDE.md');
|
||||
expect(fs.existsSync(claudePath)).toBe(true);
|
||||
if (skillsGenerated) {
|
||||
const claudeContent = fs.readFileSync(claudePath, 'utf-8');
|
||||
expect(claudeContent).toContain('.claude/skills/generated/');
|
||||
expect(claudeContent).toContain('.claude/skills/gitnexus-area-');
|
||||
}
|
||||
|
||||
const agentsPath = path.join(tmpDir, 'AGENTS.md');
|
||||
expect(fs.existsSync(agentsPath)).toBe(true);
|
||||
if (skillsGenerated) {
|
||||
const agentsContent = fs.readFileSync(agentsPath, 'utf-8');
|
||||
expect(agentsContent).toContain('.claude/skills/generated/');
|
||||
expect(agentsContent).toContain('.claude/skills/gitnexus-area-');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2393,17 +2403,20 @@ export function createEntry(level: string, msg: string) {
|
|||
].join('\n'),
|
||||
).toBe(0);
|
||||
|
||||
const generatedDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
||||
expect(fs.existsSync(generatedDir)).toBe(true);
|
||||
const skillsRoot = path.join(tmpDir, '.claude', 'skills');
|
||||
expect(fs.existsSync(skillsRoot)).toBe(true);
|
||||
|
||||
const skillDirs = fs
|
||||
.readdirSync(generatedDir)
|
||||
.filter((d) => fs.statSync(path.join(generatedDir, d)).isDirectory());
|
||||
.readdirSync(skillsRoot)
|
||||
.filter(
|
||||
(d) =>
|
||||
d.startsWith('gitnexus-area-') && fs.statSync(path.join(skillsRoot, d)).isDirectory(),
|
||||
);
|
||||
expect(skillDirs.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
/* All SKILL.md files should still have valid frontmatter */
|
||||
for (const dir of skillDirs) {
|
||||
const skillPath = path.join(generatedDir, dir, 'SKILL.md');
|
||||
const skillPath = path.join(skillsRoot, dir, 'SKILL.md');
|
||||
expect(fs.existsSync(skillPath)).toBe(true);
|
||||
const content = fs.readFileSync(skillPath, 'utf-8');
|
||||
expect(content.startsWith('---')).toBe(true);
|
||||
|
|
@ -2416,6 +2429,6 @@ export function createEntry(level: string, msg: string) {
|
|||
const claudePath = path.join(tmpDir, 'CLAUDE.md');
|
||||
expect(fs.existsSync(claudePath)).toBe(true);
|
||||
const claudeContent = fs.readFileSync(claudePath, 'utf-8');
|
||||
expect(claudeContent).toContain('.claude/skills/generated/');
|
||||
expect(claudeContent).toContain('.claude/skills/gitnexus-area-');
|
||||
}, 90000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -343,26 +343,69 @@ Old content here.
|
|||
expect(result).not.toContain('Old content here');
|
||||
});
|
||||
|
||||
it('installs skills files', async () => {
|
||||
it('installs standard skills as direct children of .claude/skills (#2433)', async () => {
|
||||
const stats = { nodes: 10 };
|
||||
await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);
|
||||
|
||||
// Should have installed skill files
|
||||
const skillsDir = path.join(tmpDir, '.claude', 'skills', 'gitnexus');
|
||||
const standardSkills = [
|
||||
'gitnexus-exploring',
|
||||
'gitnexus-debugging',
|
||||
'gitnexus-impact-analysis',
|
||||
'gitnexus-refactoring',
|
||||
'gitnexus-guide',
|
||||
'gitnexus-cli',
|
||||
];
|
||||
for (const skill of standardSkills) {
|
||||
await expect(
|
||||
fs.access(path.join(tmpDir, '.claude', 'skills', skill, 'SKILL.md')),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
fs.access(path.join(tmpDir, '.claude', 'skills', 'gitnexus', skill, 'SKILL.md')),
|
||||
).rejects.toThrow();
|
||||
}
|
||||
|
||||
const claudeContent = generateGitNexusContent('TestProject', stats);
|
||||
expect(claudeContent).toContain('.claude/skills/gitnexus-exploring/SKILL.md');
|
||||
expect(claudeContent).not.toContain('.claude/skills/gitnexus/gitnexus-exploring/SKILL.md');
|
||||
});
|
||||
|
||||
it('migrates known nested standard skills without deleting user-owned siblings (#2433)', async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-ai-ctx-skill-migrate-'));
|
||||
const storage = path.join(dir, '.gitnexus');
|
||||
const legacyKnown = path.join(dir, '.claude', 'skills', 'gitnexus', 'gitnexus-exploring');
|
||||
const legacyUnknown = path.join(dir, '.claude', 'skills', 'gitnexus', 'custom-team-skill');
|
||||
const unrelated = path.join(dir, '.claude', 'skills', 'auth');
|
||||
await fs.mkdir(legacyKnown, { recursive: true });
|
||||
await fs.mkdir(legacyUnknown, { recursive: true });
|
||||
await fs.mkdir(unrelated, { recursive: true });
|
||||
await fs.writeFile(path.join(legacyKnown, 'SKILL.md'), 'legacy', 'utf-8');
|
||||
await fs.writeFile(path.join(legacyUnknown, 'SKILL.md'), 'custom nested', 'utf-8');
|
||||
await fs.writeFile(path.join(unrelated, 'SKILL.md'), 'custom direct', 'utf-8');
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(skillsDir, { recursive: true });
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
} catch {
|
||||
// Skills dir may not be created if skills source doesn't exist in test context
|
||||
await generateAIContextFiles(dir, storage, 'TestProject', { nodes: 10 });
|
||||
|
||||
await expect(
|
||||
fs.access(path.join(dir, '.claude', 'skills', 'gitnexus-exploring', 'SKILL.md')),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(fs.access(legacyKnown)).rejects.toThrow();
|
||||
await expect(fs.readFile(path.join(legacyUnknown, 'SKILL.md'), 'utf-8')).resolves.toBe(
|
||||
'custom nested',
|
||||
);
|
||||
await expect(fs.readFile(path.join(unrelated, 'SKILL.md'), 'utf-8')).resolves.toBe(
|
||||
'custom direct',
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('does not create .claude/skills/gitnexus/ when skipSkills is true (#742)', async () => {
|
||||
it('does not create standard skill directories when skipSkills is true (#742)', async () => {
|
||||
// Regression guard for #742. The --skip-skills flag must prevent
|
||||
// installSkills() from writing the 6 standard skill dirs into the
|
||||
// analyzed repo. Per-test tmpdir so we start from a known-clean
|
||||
// slate — the shared tmpDir from beforeAll may already contain
|
||||
// .claude/skills/gitnexus/ from an earlier test.
|
||||
// direct .claude/skills/gitnexus-* directories from an earlier test.
|
||||
const skipDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-ai-ctx-skip-skills-'));
|
||||
const skipStorage = path.join(skipDir, '.gitnexus');
|
||||
await fs.mkdir(skipStorage, { recursive: true });
|
||||
|
|
@ -377,9 +420,9 @@ Old content here.
|
|||
{ skipSkills: true },
|
||||
);
|
||||
|
||||
expect(result.files).toContain('.claude/skills/gitnexus/ (skipped via --skip-skills)');
|
||||
expect(result.files).toContain('.claude/skills/gitnexus-*/ (skipped via --skip-skills)');
|
||||
await expect(
|
||||
fs.access(path.join(skipDir, '.claude', 'skills', 'gitnexus')),
|
||||
fs.access(path.join(skipDir, '.claude', 'skills', 'gitnexus-exploring')),
|
||||
).rejects.toThrow();
|
||||
} finally {
|
||||
await fs.rm(skipDir, { recursive: true, force: true });
|
||||
|
|
@ -408,11 +451,13 @@ Old content here.
|
|||
|
||||
expect(result.files).toContain('AGENTS.md (skipped via --skip-agents-md)');
|
||||
expect(result.files).toContain('CLAUDE.md (skipped via --skip-agents-md)');
|
||||
expect(result.files).toContain('.claude/skills/gitnexus/ (skipped via --skip-skills)');
|
||||
expect(result.files).toContain('.claude/skills/gitnexus-*/ (skipped via --skip-skills)');
|
||||
|
||||
await expect(fs.access(path.join(idxDir, 'AGENTS.md'))).rejects.toThrow();
|
||||
await expect(fs.access(path.join(idxDir, 'CLAUDE.md'))).rejects.toThrow();
|
||||
await expect(fs.access(path.join(idxDir, '.claude', 'skills', 'gitnexus'))).rejects.toThrow();
|
||||
await expect(
|
||||
fs.access(path.join(idxDir, '.claude', 'skills', 'gitnexus-exploring')),
|
||||
).rejects.toThrow();
|
||||
} finally {
|
||||
await fs.rm(idxDir, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -420,7 +465,7 @@ Old content here.
|
|||
|
||||
it('omits standard skill references from AGENTS.md/CLAUDE.md when skipSkills is true (#742)', async () => {
|
||||
// The skills routing table in AGENTS.md/CLAUDE.md points agents at
|
||||
// .claude/skills/gitnexus/*/SKILL.md files installed by installSkills().
|
||||
// .claude/skills/gitnexus-*/SKILL.md files installed by installSkills().
|
||||
// When --skip-skills suppresses that install but AGENTS.md/CLAUDE.md
|
||||
// are still written, the routing table must NOT name files that don't
|
||||
// exist — otherwise every agent load incurs 6 failed reads and the
|
||||
|
|
@ -452,6 +497,28 @@ Old content here.
|
|||
}
|
||||
});
|
||||
|
||||
it('keeps direct community skill paths when standard skills are skipped (#2433)', () => {
|
||||
const content = generateGitNexusContent(
|
||||
'TestProject',
|
||||
{ nodes: 50, edges: 100, processes: 5 },
|
||||
{
|
||||
skipSkills: true,
|
||||
generatedSkills: [
|
||||
{
|
||||
name: 'gitnexus-area-auth',
|
||||
label: 'Auth',
|
||||
symbolCount: 40,
|
||||
fileCount: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expect(content).toContain('.claude/skills/gitnexus-area-auth/SKILL.md');
|
||||
expect(content).not.toContain('.claude/skills/gitnexus-exploring/SKILL.md');
|
||||
expect(content).not.toContain('.claude/skills/generated/');
|
||||
});
|
||||
|
||||
it('preserves manual AGENTS.md and CLAUDE.md edits when skipAgentsMd is enabled', async () => {
|
||||
const stats = { nodes: 42, edges: 84, processes: 3 };
|
||||
const agentsPath = path.join(tmpDir, 'AGENTS.md');
|
||||
|
|
@ -967,7 +1034,7 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust
|
|||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Work in the Auth area (40 symbols) | \`.claude/skills/generated/auth/SKILL.md\` |
|
||||
| Work in the Auth area (40 symbols) | \`.claude/skills/gitnexus-area-auth/SKILL.md\` |
|
||||
<!-- gitnexus:end -->
|
||||
`;
|
||||
for (const f of ['AGENTS.md', 'CLAUDE.md']) {
|
||||
|
|
@ -982,7 +1049,7 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust
|
|||
expect(after).toContain('base_ref: "develop"');
|
||||
expect(after).not.toContain('base_ref: "main"');
|
||||
// The community-skill row (and everything else) is preserved.
|
||||
expect(after).toContain('.claude/skills/generated/auth/SKILL.md');
|
||||
expect(after).toContain('.claude/skills/gitnexus-area-auth/SKILL.md');
|
||||
}
|
||||
|
||||
// Idempotent: a second run with the same branch writes nothing.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const {
|
|||
refreshBaseRefLineMock: vi.fn(async () => ({ files: [] as string[] })),
|
||||
generateSkillFilesMock: vi.fn(async () => ({
|
||||
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
|
||||
outputPath: '/repo/.claude/skills/generated',
|
||||
outputPath: '/repo/.claude/skills',
|
||||
})),
|
||||
cliErrorMock: vi.fn(),
|
||||
getDefaultBranchMock: vi.fn<(p: string) => string | null>(() => null),
|
||||
|
|
@ -88,7 +88,7 @@ describe('analyzeCommand .gitnexusrc wiring (#243)', () => {
|
|||
generateSkillFilesMock.mockReset();
|
||||
generateSkillFilesMock.mockResolvedValue({
|
||||
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
|
||||
outputPath: '/repo/.claude/skills/generated',
|
||||
outputPath: '/repo/.claude/skills',
|
||||
});
|
||||
cliErrorMock.mockReset();
|
||||
getDefaultBranchMock.mockReset();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const { runFullAnalysisMock, generateAIContextFilesMock, generateSkillFilesMock,
|
|||
const generateAIContextFilesMock = vi.fn(async () => ({ files: [] as string[] }));
|
||||
const generateSkillFilesMock = vi.fn(async () => ({
|
||||
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
|
||||
outputPath: '/repo/.claude/skills/generated',
|
||||
outputPath: '/repo/.claude/skills',
|
||||
}));
|
||||
const cliErrorMock = vi.fn();
|
||||
return {
|
||||
|
|
@ -74,7 +74,7 @@ describe('analyzeCommand commander → runFullAnalysis noStats bridge (#1477)',
|
|||
generateSkillFilesMock.mockReset();
|
||||
generateSkillFilesMock.mockResolvedValue({
|
||||
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
|
||||
outputPath: '/repo/.claude/skills/generated',
|
||||
outputPath: '/repo/.claude/skills',
|
||||
});
|
||||
cliErrorMock.mockReset();
|
||||
process.exitCode = undefined;
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ describe('generateSkillFiles — return values', () => {
|
|||
);
|
||||
|
||||
expect(result.skills).toEqual([]);
|
||||
expect(result.outputPath).toBe(path.join(tmpDir, '.claude', 'skills', 'generated'));
|
||||
expect(result.outputPath).toBe(path.join(tmpDir, '.claude', 'skills'));
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -252,7 +252,7 @@ describe('generateSkillFiles — return values', () => {
|
|||
expect(result.skills[0].label).toBe('Auth');
|
||||
expect(result.skills[0].symbolCount).toBe(5);
|
||||
expect(result.skills[0].fileCount).toBe(2);
|
||||
expect(result.skills[0].name).toBe('auth');
|
||||
expect(result.skills[0].name).toBe('gitnexus-area-auth');
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -573,10 +573,10 @@ describe('generateSkillFiles — file output', () => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Verify that each community produces a directory under generated/
|
||||
* Verify that each community produces a namespaced directory directly under .claude/skills/
|
||||
* containing a SKILL.md file.
|
||||
*/
|
||||
it('creates generated/{name}/SKILL.md for each community', async () => {
|
||||
it('creates {name}/SKILL.md as a direct project skill for each community (#2433)', async () => {
|
||||
const { graph, communities, memberships } = twoCommSetup();
|
||||
|
||||
await generateSkillFiles(
|
||||
|
|
@ -590,13 +590,59 @@ describe('generateSkillFiles — file output', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
const outputDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
||||
const alphaSkill = await fs.readFile(path.join(outputDir, 'alpha', 'SKILL.md'), 'utf-8');
|
||||
const betaSkill = await fs.readFile(path.join(outputDir, 'beta', 'SKILL.md'), 'utf-8');
|
||||
const outputDir = path.join(tmpDir, '.claude', 'skills');
|
||||
const alphaSkill = await fs.readFile(
|
||||
path.join(outputDir, 'gitnexus-area-alpha', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
const betaSkill = await fs.readFile(
|
||||
path.join(outputDir, 'gitnexus-area-beta', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(alphaSkill.length).toBeGreaterThan(0);
|
||||
expect(betaSkill.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('uses an owned namespace and removes only prior GitNexus-generated outputs (#2433)', async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
graph.addNode(
|
||||
makeNode(`fn:cli${i}`, `cliFn${i}`, 'Function', `${tmpDir}/src/cli/f${i}.ts`, 1, true),
|
||||
);
|
||||
}
|
||||
const skillsRoot = path.join(tmpDir, '.claude', 'skills');
|
||||
const standardSkill = path.join(skillsRoot, 'gitnexus-cli', 'SKILL.md');
|
||||
const userSkill = path.join(skillsRoot, 'auth', 'SKILL.md');
|
||||
const legacyGenerated = path.join(skillsRoot, 'generated', 'old', 'SKILL.md');
|
||||
const staleGenerated = path.join(skillsRoot, 'gitnexus-area-old', 'SKILL.md');
|
||||
for (const file of [standardSkill, userSkill, legacyGenerated, staleGenerated]) {
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, file, 'utf-8');
|
||||
}
|
||||
|
||||
const result = await generateSkillFiles(
|
||||
tmpDir,
|
||||
'TestProject',
|
||||
buildPipelineResult({
|
||||
graph,
|
||||
repoPath: tmpDir,
|
||||
communities: [makeCommunity('c1', 'Cli', 4)],
|
||||
memberships: [0, 1, 2, 3].map((i) => makeMembership(`fn:cli${i}`, 'c1')),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.skills[0].name).toBe('gitnexus-area-cli');
|
||||
const generatedContent = await fs.readFile(
|
||||
path.join(skillsRoot, 'gitnexus-area-cli', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(generatedContent).toContain('name: gitnexus-area-cli');
|
||||
await expect(fs.readFile(standardSkill, 'utf-8')).resolves.toBe(standardSkill);
|
||||
await expect(fs.readFile(userSkill, 'utf-8')).resolves.toBe(userSkill);
|
||||
await expect(fs.access(path.join(skillsRoot, 'generated'))).rejects.toThrow();
|
||||
await expect(fs.access(path.join(skillsRoot, 'gitnexus-area-old'))).rejects.toThrow();
|
||||
});
|
||||
|
||||
/**
|
||||
* SKILL.md files should start with YAML frontmatter containing
|
||||
* name and description fields.
|
||||
|
|
@ -616,7 +662,7 @@ describe('generateSkillFiles — file output', () => {
|
|||
);
|
||||
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', 'generated', 'alpha', 'SKILL.md'),
|
||||
path.join(tmpDir, '.claude', 'skills', 'gitnexus-area-alpha', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(content.startsWith('---')).toBe(true);
|
||||
|
|
@ -645,7 +691,7 @@ describe('generateSkillFiles — file output', () => {
|
|||
);
|
||||
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', 'generated', 'alpha', 'SKILL.md'),
|
||||
path.join(tmpDir, '.claude', 'skills', 'gitnexus-area-alpha', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(content).not.toMatch(/gitnexus_(context|query|impact|detect_changes|rename|cypher)/);
|
||||
|
|
@ -700,7 +746,7 @@ describe('generateSkillFiles — file output', () => {
|
|||
);
|
||||
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', 'generated', 'alpha', 'SKILL.md'),
|
||||
path.join(tmpDir, '.claude', 'skills', 'gitnexus-area-alpha', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
|
@ -737,7 +783,7 @@ describe('generateSkillFiles — file output', () => {
|
|||
);
|
||||
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', 'generated', 'isolated', 'SKILL.md'),
|
||||
path.join(tmpDir, '.claude', 'skills', 'gitnexus-area-isolated', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
|
@ -770,9 +816,9 @@ describe('generateSkillFiles — file output', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
const outputDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
||||
const outputDir = path.join(tmpDir, '.claude', 'skills');
|
||||
const firstRunDirs = await fs.readdir(outputDir);
|
||||
expect(firstRunDirs).toContain('first');
|
||||
expect(firstRunDirs).toContain('gitnexus-area-first');
|
||||
|
||||
// Second run with different community
|
||||
const graph2 = createKnowledgeGraph();
|
||||
|
|
@ -794,8 +840,8 @@ describe('generateSkillFiles — file output', () => {
|
|||
);
|
||||
|
||||
const secondRunDirs = await fs.readdir(outputDir);
|
||||
expect(secondRunDirs).toContain('second');
|
||||
expect(secondRunDirs).not.toContain('first');
|
||||
expect(secondRunDirs).toContain('gitnexus-area-second');
|
||||
expect(secondRunDirs).not.toContain('gitnexus-area-first');
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -825,7 +871,7 @@ describe('generateSkillFiles — file output', () => {
|
|||
);
|
||||
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', 'generated', 'stats', 'SKILL.md'),
|
||||
path.join(tmpDir, '.claude', 'skills', 'gitnexus-area-stats', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
|
@ -862,18 +908,54 @@ describe('generateSkillFiles — file output', () => {
|
|||
// The kebab name should only contain lowercase alphanumerics and dashes
|
||||
expect(result.skills[0].name).toMatch(/^[a-z0-9-]+$/);
|
||||
|
||||
const skillPath = path.join(
|
||||
tmpDir,
|
||||
'.claude',
|
||||
'skills',
|
||||
'generated',
|
||||
result.skills[0].name,
|
||||
'SKILL.md',
|
||||
);
|
||||
const skillPath = path.join(tmpDir, '.claude', 'skills', result.skills[0].name, 'SKILL.md');
|
||||
const content = await fs.readFile(skillPath, 'utf-8');
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps colliding names within Claude Code's 64-character limit", async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
for (let i = 0; i < 8; i++) {
|
||||
graph.addNode(
|
||||
makeNode(
|
||||
`fn:long${i}`,
|
||||
`longFunc${i}`,
|
||||
'Function',
|
||||
`${tmpDir}/src/long/f${i}.ts`,
|
||||
1,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const sharedPrefix = 'a'.repeat(60);
|
||||
const communities = [
|
||||
makeCommunity('c1', `${sharedPrefix}one`, 4),
|
||||
makeCommunity('c2', `${sharedPrefix}two`, 4),
|
||||
];
|
||||
const memberships = [
|
||||
...[0, 1, 2, 3].map((i) => makeMembership(`fn:long${i}`, 'c1')),
|
||||
...[4, 5, 6, 7].map((i) => makeMembership(`fn:long${i}`, 'c2')),
|
||||
];
|
||||
|
||||
const result = await generateSkillFiles(
|
||||
tmpDir,
|
||||
'TestProject',
|
||||
buildPipelineResult({ graph, repoPath: tmpDir, communities, memberships }),
|
||||
);
|
||||
|
||||
expect(result.skills).toHaveLength(2);
|
||||
expect(new Set(result.skills.map((skill) => skill.name)).size).toBe(2);
|
||||
for (const skill of result.skills) {
|
||||
expect(skill.name.length).toBeLessThanOrEqual(64);
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', skill.name, 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(content).toContain(`name: ${skill.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Nodes with no filePath should not crash the generator.
|
||||
* The skill should still be generated with fileCount 0.
|
||||
|
|
@ -933,7 +1015,7 @@ describe('generateSkillFiles — file output', () => {
|
|||
expect(result.skills).toHaveLength(1);
|
||||
|
||||
const content = await fs.readFile(
|
||||
path.join(tmpDir, '.claude', 'skills', 'generated', 'win', 'SKILL.md'),
|
||||
path.join(tmpDir, '.claude', 'skills', 'gitnexus-area-win', 'SKILL.md'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); // -> monorepo root
|
|||
|
||||
function collectSkillFiles(): string[] {
|
||||
const files: string[] = [];
|
||||
const projectSkillsRoot = path.join(REPO_ROOT, '.claude', 'skills');
|
||||
|
||||
// Bundled ship source: flat *.md files installSkills() copies to new users.
|
||||
const bundled = path.join(GITNEXUS_ROOT, 'skills');
|
||||
|
|
@ -32,13 +33,17 @@ function collectSkillFiles(): string[] {
|
|||
|
||||
// Per-skill <name>/SKILL.md copies across the other distribution locations.
|
||||
const skillRoots = [
|
||||
path.join(REPO_ROOT, '.claude', 'skills', 'gitnexus'),
|
||||
projectSkillsRoot,
|
||||
path.join(projectSkillsRoot, 'gitnexus'),
|
||||
path.join(REPO_ROOT, 'gitnexus-claude-plugin', 'skills'),
|
||||
path.join(REPO_ROOT, 'gitnexus-cursor-integration', 'skills'),
|
||||
];
|
||||
for (const root of skillRoots) {
|
||||
if (!existsSync(root)) continue;
|
||||
for (const dir of readdirSync(root)) {
|
||||
if (root === projectSkillsRoot && !dir.startsWith('gitnexus-')) {
|
||||
continue;
|
||||
}
|
||||
const skillMd = path.join(root, dir, 'SKILL.md');
|
||||
if (existsSync(skillMd)) files.push(skillMd);
|
||||
}
|
||||
|
|
@ -57,11 +62,16 @@ function cliSkillFiles(files: string[]): string[] {
|
|||
describe('skill-file steering (#1939, #1945)', () => {
|
||||
const files = collectSkillFiles();
|
||||
|
||||
it('collects skill files from all four committed locations (guard is not vacuous)', () => {
|
||||
it('collects skill files from all committed locations (guard is not vacuous)', () => {
|
||||
const rels = files.map((f) => path.relative(REPO_ROOT, f));
|
||||
expect(rels.some((r) => r.startsWith(`gitnexus${path.sep}skills${path.sep}`))).toBe(true);
|
||||
expect(
|
||||
rels.some((r) => r.startsWith(path.join('.claude', 'skills', 'gitnexus') + path.sep)),
|
||||
rels.some((r) => r.startsWith(path.join('.claude', 'skills', 'gitnexus-cli') + path.sep)),
|
||||
).toBe(true);
|
||||
expect(
|
||||
rels.some((r) =>
|
||||
r.startsWith(path.join('.claude', 'skills', 'gitnexus', 'gitnexus-pdg-query') + path.sep),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
rels.some((r) => r.startsWith(path.join('gitnexus-claude-plugin', 'skills') + path.sep)),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ describe('--skip-git CLI flag', () => {
|
|||
expect(helpOutput).toContain('--skip-git');
|
||||
expect(helpOutput).toContain('--skip-agents-md');
|
||||
expect(helpOutput).toContain('--skip-skills');
|
||||
expect(helpOutput).toContain('directly under .claude/skills/');
|
||||
expect(helpOutput).toContain('.claude/skills/gitnexus-area-*');
|
||||
expect(helpOutput).toContain('--index-only');
|
||||
expect(helpOutput).not.toContain('--no-git');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue