How to propose changes, run checks locally, and open pull requests.
This project uses the PolyForm Noncommercial License 1.0.0. By contributing, you agree your contributions are licensed under the same terms unless stated otherwise.
- Issues & feature ideas: use GitHub Issues for the upstream repo, or your fork’s tracker if you work from a fork.
- Community: see the Discord link in the root README.md.
Prerequisites: Node.js — gitnexus/ requires >=22.0.0 and gitnexus-web/ requires ^20.19.0 || >=22.12.0 (enforced via the engines field in each package). Use nvm install to match the local version.
- Clone the repository.
- Shared package:
cd gitnexus-shared && npm install && npm run build - CLI / MCP package:
cd ../gitnexus && npm install && npm run build - Web UI (if needed):
cd ../gitnexus-web && npm install - Run tests as described in TESTING.md.
The CLI build imports gitnexus-shared, so a fresh clone must install and build
the shared package before running npm install in gitnexus/. This is the same
order used by the repository's setup-gitnexus CI action.
If you prefer an isolated environment with Claude Code, OpenAI Codex CLI, and Cursor CLI pre-installed, open the repo in VS Code with the Dev Containers extension and run Dev Containers: Reopen in Container. See .devcontainer/README.md for first-time auth flows and Windows WSL2 setup.
- Use short-lived branches off the default branch of the repo you are targeting.
- PR titles MUST follow the conventional-commit format —
pr-labeler.ymlenforces this on every PR and auto-applies the matching label so release notes group the change correctly. - PR description: what changed, why, how to verify (commands), and any risk or rollback notes.
Format: <type>[(scope)][!]: <subject>
Allowed types and the release-notes section each one lands in (defined in .github/release.yml):
| Type | Label applied | Release-notes section |
|---|---|---|
feat |
enhancement |
🚀 Features |
fix |
bug |
🐛 Bug Fixes |
perf |
performance |
🏎️ Performance |
refactor |
refactor |
🔄 Refactoring |
test |
test |
🧪 Tests |
ci |
ci |
👷 CI/CD |
build / deps |
dependencies |
📦 Dependencies |
docs |
documentation |
(grouped under Other Changes unless a Docs section is added) |
chore / revert |
chore |
(excluded from release notes) |
Append ! to the type (e.g. feat(api)!: drop /v1 endpoint) or include BREAKING CHANGE: in the PR body to flag a breaking change — the labeler then adds the breaking label and the 💥 Breaking Changes section is rendered first.
Examples:
feat(web): add smart chat scroll
fix(extractors): resolve silent contract mis-resolution
perf: avoid O(n²) traversal in heritage walker
chore(deps): bump vitest to 3.0.0
ci: standardize workflow concurrency
Commits within a PR may use any style — only the merged PR title shows up in release notes, so that's the one the convention applies to.
- Tests pass for the packages you touched (
gitnexusand/orgitnexus-web). - Typecheck passes:
npx tsc --noEmitingitnexus/andnpx tsc -b --noEmitingitnexus-web/. - No secrets, tokens, or machine-specific paths committed.
- Documentation updated if behavior or public CLI/MCP contract changes.
- Pre-commit hook runs clean (
.husky/pre-commit— formatting via lint-staged + typecheck for staged packages; tests run in CI only).
Maintainers may request changes for correctness, tests, performance, or consistency with existing patterns. Keeping diffs focused makes review faster.
Every workflow under .github/workflows/ MUST declare a top-level concurrency: block using this convention:
-
Group key starts with
${{ github.workflow }}so no two workflows can collide on the same group name. The discriminator that follows is chosen per event shape:- Branch/tag scope:
${{ github.workflow }}-${{ github.ref }} - Per-PR scope (for
issue_comment,pull_request_review*,pull_requestmeta events):${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number }} workflow_runscope (e.g.ci-report.yml):${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) }}— the fork fallback must be stable across reruns (neverworkflow_run.id, which is per-run-unique and defeats serialization).- Global single-slot (manual dispatch utilities):
${{ github.workflow }} - Reusable workflows invoked via
workflow_call: do NOT use${{ github.workflow }}in the group key — in called-workflow context its evaluation is ambiguous and can resolve to the caller's name, which would deadlock against the caller's own group. Use a hardcoded literal prefix and agithub.event_name-aware expression that falls through togithub.run_idfor reusable invocations (seeci.ymlfor the canonical form). Approved literal prefixes:CI-(ci.yml) anddocker-build-push-(docker.yml). Thecheck-workflow-concurrency.pyvalidation script must be updated whenever a new approved literal prefix is added. - Merge queue (
merge_group): when this event is added, use${{ github.workflow }}-${{ github.event.merge_group.head_ref }}withcancel-in-progress: false(every queue entry is a distinct ref; never cancel).
- Branch/tag scope:
-
cancel-in-progresspolicy:Event cancel-in-progressWhy pull_requestCI runtrueNew push supersedes old run pushtomainfalseEvery main commit gets validated Tag push ( v*publish)falseNever cancel mid-publish pushtomainfor release-candidatefalseNever cancel mid-RC publish workflow_dispatch(release/publish)falseManual runs are intentional workflow_run(sticky-comment reports)falseSerialize, don't race Per-PR bot workflows ( @claude, review)falseSerialize comments per PR PR-meta re-checks (pr-description-check) trueCheap, latest wins Single-slot utilities (triage sweep) trueLatest dispatch supersedes -
For workflows that serve multiple events at once (e.g.
ci.ymlhandlespull_request,push, andworkflow_call), makecancel-in-progressevent-aware:concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }}
-
When adding a new workflow, copy the concurrency block from an existing workflow of the same event shape.
Two workflows produce machine-readable signals on every PR. Coding agents and humans alike can rely on the names and shapes below — change them with intent.
pr-autofix.yml (untrusted) + pr-autofix-publish.yml (trusted) run prettier --write and eslint --fix against the PR head and surface a single ChatOps button on the PR. Three signals are emitted:
| Surface | Where | Notes |
|---|---|---|
| Sticky PR comment | Top-level comment with the HTML marker <!-- gitnexus:pr-autofix-summary --> and heading ## :sparkles: PR Autofix. Only posted when there is something to fix; clean PRs stay silent. |
Edit-in-place via marker; one comment per PR. |
| Fenced JSON block | Inside the sticky, fenced as gitnexus-autofix. Schema gitnexus.pr-autofix/v2 with fields state (fixes-available), pr_number, head_sha, changed_lines, run_id, and apply_command (literal /autofix). |
Parseable signal — preferred over regexing prose. v1 fields preserved as a superset. |
| Check Run | Stable name gitnexus/autofix on the PR head SHA. Conclusion: success (clean) or neutral (fixes-available). The neutral title is Autofix available — comment /autofix to apply. |
Surfaced under PR Checks; readable via gh pr checks <pr>. |
To detect outcome from an agent: gh pr checks <pr> --json name,conclusion,output | jq '.[] | select(.name == "gitnexus/autofix")'.
Forks are supported. The untrusted half runs fork code with permissions: {} and ships the diff as an artifact; the trusted publish job consumes only the diff (data, not code) and posts the comment + check run.
Comment /autofix on the PR (whole-line, no arguments). The pr-autofix-apply.yml workflow:
- Validates the comment body matches
^/autofix\s*$exactly. Quoted or inline mentions are silently ignored. - Validates the commenter has
admin,write, ormaintainpermission on the repo, OR is the PR author. Other commenters get a 👎 reaction and a refusal reply. - Locates the most recent successful
pr-autofix.ymlrun for the PR's current head SHA, downloads itsautofixartifact, applies the patch, and pushes achore(autofix): ...commit back to the PR head branch. - Reacts ✅ on success, 👎 on stale-patch / push-failure, and posts a short reply with the apply-run URL in either case.
The apply workflow runs from the default branch's copy of the file regardless of where the comment originates — that's the trust anchor. There is no diff-size cap (the apply workflow uses git apply + push, not the GitHub review-comment API).
For fork PRs, the push succeeds only when the contributor has Allow edits by maintainers enabled on the PR (the default). When they have disabled it, the workflow fails loud with a 👎 reaction and an explanation comment.
Re-invoking /autofix after a successful apply is a safe no-op — the workflow detects the already-applied state via git apply --check --reverse and reacts ✅ without pushing.
Sensitive paths. The apply workflow refuses any patch that touches .github/ (workflow files, CODEOWNERS, dependabot config). A malicious PR could ship a custom prettier or ESLint config that reformats workflow YAML; if accepted, those edits would be pushed under contents: write without human review. Apply formatter changes to files under .github/ manually in a normal commit so they get the same review every other workflow change gets.
.github/vendored-grammars.json is the single source of truth for the vendored tree-sitter grammar set and each grammar's policy hold (the ones shipped from gitnexus/vendor/<name> rather than installed from npm). It lists each grammar's name, upstream coords (npm or github), and any hold. The monitor resolves upstreams from it; the readiness report keeps its own upstream-drift coords and reads vendored ABIs from gitnexus/vendor/. Two workflows read it:
grammar-update-monitor.yml(.github/scripts/update-vendored-grammars.mjs) — weekly; opens auto-PRs re-vendoring ABI-compatible upstream updates.tree-sitter-upgrade-readiness.yml(.github/scripts/check-tree-sitter-upgrade-readiness.py) — daily; renders the tree-sitter-0.25 readiness report (issue #858), reading each vendored grammar's ABI fromgitnexus/vendor/<name>/src/parser.c.
Sharing the manifest keeps the two aligned: a consistency-guard test asserts the manifest set equals the gitnexus/vendor/tree-sitter-* directories. When you vendor a new grammar (or remove one), update .github/vendored-grammars.json in the same change — otherwise that guard fails CI and the readiness report regresses to ? placeholders.
If you use coding agents, follow project context files (e.g. AGENTS.md, CLAUDE.md) and avoid drive-by refactors unrelated to the issue. Prefer incremental, test-backed changes.
One workflow ships gitnexus to npm — .github/workflows/publish.yml. It
routes between two modes based on the triggering event:
-
Stable mode — triggered by pushing any
v<X.Y.Z>tag (no-rc.*suffix; RC tags are excluded at trigger via a negative glob). Publishes to thelatestdist-tag with a changelog-backed GitHub release. Maintainers are expected to tag frommainas a convention; the workflow itself does not enforce branch reachability. No Docker build (RC-only). Before cutting a stable release, keepgitnexus/package.json,gitnexus-claude-plugin/.claude-plugin/plugin.json,.claude-plugin/marketplace.json,gitnexus-claude-plugin/.codex-plugin/plugin.json,.agents/plugins/marketplace.json, and the matchingCHANGELOG.mdentry in lockstep — the always-ongitnexusunit suite now fails if those manifest versions drift. -
Release-candidate mode — runs on every push to
main(typically a merged PR) plus manualworkflow_dispatch. Docs-only changes are skipped viapaths-ignore. Publishes to thercdist-tag with versionX.Y.Z-rc.Nand a GitHub prerelease, where:X.Y.Zis selected automatically. On push (and on dispatch withbump: auto, the default) the workflow continues the active rc cycle: if the registry already hasX.Y.Z-rc.*versions withX.Y.Z> currentlatest, it reuses the highest such base; otherwise it patch-bumps fromlatest. Dispatching withbump: patch|minor|majorresets the cycle fromlatest.Nis auto-incremented against existingX.Y.Z-rc.*entries on the registry. First rc for a given base isrc.1.- After the npm publish succeeds, the workflow calls
docker.ymlas a reusable workflow to build and push the corresponding RC Docker images (e.g.ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1, mirrored todocker.io/akonlabs/gitnexus:1.7.0-rc.1). The images are signed with Cosign; the OIDC identity isdocker.yml@refs/heads/main(the caller's ref — see README.md § Docker for the verify command).
Idempotency: the workflow pushes an
rc/<HEAD_SHA>marker tag and av<RC>release tag atomically, before callingnpm publish. The RC guard refuses to re-run once the marker exists, so a post-publish failure will not mint a duplicate rc for the same commit. Thev<RC>tag points at a detached release commit whosepackage.jsonmatches the npm tarball exactly (traceable releases). The RC tag is excluded from this workflow'spush: tags:filter, so it does not re-trigger publishing — preventing the double-publish failure mode tracked in #1609. Recovery after a partial failure: the workflow'sif: failure()cleanup step in thepublishjob auto-deletes the v-tag and marker on most post-publish failures, so the typical retry is just:gh workflow run publish.yml --ref main -f force=true # or push a new commit to main, which will cut a fresh RCIf auto-cleanup didn't run (e.g. the cleanup step itself failed, or the failure happened in the route/rc-guard phase before the marker was pushed), manual cleanup is:
git push --delete origin rc/<HEAD_SHA> v<RC> # then redispatch with force: true
Release-PR-skip subject pattern. The rc-guard job recognizes a squash-merged release commit by matching the commit subject against
^chore: release vX.Y.Z(optionally followed by(#NNNN)for the squash-merge PR-number suffix). Match is case-insensitive —Chore: Release v1.2.3works too. PRs that should suppress the RC build must either use this subject shape, or carry thereleaselabel so the label-based fallback fires. Other release-style subjects (chore(release): v1.2.3,release: v1.2.3) will NOT trigger the skip — please name the release PR exactlychore: release vX.Y.Zto keep the dedup deterministic.Docker-only partial failure: if
publishsucceeds (npm tarball + tags are live) but thedockerjob subsequently fails (e.g. GHCR flakiness), the npm RC is already published and therc/<HEAD_SHA>marker is in place. Recovery without cutting a new RC:# Re-run only the failed docker job from the original workflow run: gh run rerun <run-id> --failed
Find the run ID via
gh run list --workflow=publish.yml --branch main.docker.ymlintentionally has noworkflow_dispatchtrigger (images are tag-driven by design), so the gh-run-rerun path is the supported recovery.GitHub Release transient failure (npm publish succeeded, Release step failed): the npm artifact is live but no GitHub Release page exists. Recover by either re-running the failed job (
gh run rerun <run-id> --failed), or creating the Release manually:gh release create v<RC> --prerelease --generate-notes # RC gh release create v<X.Y.Z> --notes-file gitnexus/CHANGELOG.md # stable
The rc workflow never moves latest. To verify after a change, inspect dist-tags:
npm view gitnexus dist-tags