Commit graph

52 commits

Author SHA1 Message Date
Nilotpal Kashyap
50c6acb108
feat(setup): implement antigravity integration setup and hook adapter… (#1730)
* feat(setup): implement antigravity integration setup and hook adapter for gitnexus

* docs(readme): list Antigravity in supported editors

* test(setup-antigravity): pin platform per-test to fix Windows CI failure

The MCP entry assertion expected `npx` directly, but on Windows
`getMcpEntry()` wraps it as `cmd /c npx ...`, which broke the Windows
runner. Pin platform to darwin in beforeEach so the existing assertion
is deterministic, restore the descriptor in afterEach, and add a
parity test for the win32 cmd-wrapper shape.

* fix(antigravity): align hook adapter to Gemini CLI schema + fix Windows CI

Rebase the Antigravity integration on the canonical Gemini CLI hooks
contract (https://geminicli.com/docs/hooks/reference/), which is the
documented schema Antigravity 2.0 inherits:

- Hook adapter: replace PreToolUse/PostToolUse with the single AfterTool
  event. BeforeTool has no documented context-injection channel in the
  Gemini contract, so augmentation runs in AfterTool where
  hookSpecificOutput.additionalContext is the documented way to append
  text to the tool result the agent reads. Stale-index hints land in the
  same channel (so the agent sees them) and are mirrored to stderr for
  terminal users. Tool-name matcher updated to Gemini CLI snake_case
  (search_file_content|glob|run_shell_command).
- Setup: write hooks to ~/.gemini/settings.json under canonical
  hooks.AfterTool[] (replaces the ad-hoc hooks.json top-level group).
  Polite-neighbor merge preserves existing user hooks. Also copy
  win-rm-list-json.ps1 alongside hook-db-lock-probe.cjs so the Windows
  MCP server ownership probe doesn't silently fail open.
- Tests: 17 regression tests covering MCP write, win32 shape, hook
  schema, polite-neighbor merge, idempotency, adapter context emission,
  stale-index hint, and skill layout.
- README: footnote documenting the AfterTool design choice and a link
  to the Gemini CLI hooks reference.

Windows CI fix: installSkillsTo previously used glob('*.md') +
glob('*/SKILL.md'), which returned zero matches under the Windows
runner's temp paths (8.3 short-name like RUNNER~1). Replace with
fs.readdir + dirent type checks — same behavior, no path quirks. This
fixes the only failing Windows job on the PR.

* fix(antigravity): address PR review — windowsHide, stale docs, dead code

Addresses the production-readiness review findings on PR #1730:

- F1 (blocker): add windowsHide:true to all four spawnSync sites in the
  Antigravity hook adapter (findCanonicalRepoRoot, runGitNexusCli's two
  branches, buildStaleIndexHint) so they don't flash console windows on
  Windows. Matches the fix #1794 already on main for the Claude hook.
- F2 (blocker): update gitnexus/README.md editor table to say AfterTool
  and link the Gemini CLI hooks reference. The published README had
  drifted to the pre-c1872b4 PreToolUse + PostToolUse schema.
- F3: rewrite the stale ~/.gemini block comment in setup.ts. It still
  described the old hooks.json + gitnexus group + grep_search design.
- F4: remove grep_search dead code from extractPattern and its doc
  comment. The registered matcher is search_file_content|glob|run_shell_command,
  so grep_search would never be invoked.
- F5: annotate timeout:10000 with a ms-unit comment noting Gemini CLI
  uses milliseconds (Claude Code uses seconds).
- F6: add the GITNEXUS_DEBUG branch to extractAugmentContext for parity
  with the Claude adapter, so suppressed augment stderr is recoverable.
- F7: stageAdapter test helper now copies win-rm-list-json.ps1 alongside
  the .cjs helpers, so the adapter's Windows lock-probe path isn't a
  silent fail-open in child-process smoke tests.

* test(antigravity): add integration tests and register in cross-platform matrix

Adds end-to-end coverage on top of the unit-level tests, per maintainer
request:

- test/integration/setup-antigravity.test.ts (10 tests): exercises the
  real setupCommand() against a temp HOME with ~/.gemini/antigravity/
  present. Verifies mcp_config.json shape, ~/.gemini/settings.json
  AfterTool entry, adapter + helpers + win-rm-list-json.ps1 copy,
  baked-in cliPath rewrite (issue #108 regression class), skill layout,
  polite-neighbor merge against existing user hooks, idempotency,
  skip-when-absent, corrupt-file safety, and key preservation.
- test/integration/antigravity-hook-e2e.test.ts (19 tests): runs the
  full install-then-execute flow — invokes setupCommand to lay down
  the adapter + helpers, then spawns the INSTALLED adapter as a real
  child process against a temp git repo + .gitnexus/. The source
  adapter cannot be spawned directly (it requires sibling .cjs helpers
  that only live in hooks/claude/); install-then-spawn mirrors the
  production codepath. Covers staleness detection across all five git
  mutation types, --embeddings propagation, polite skip on
  toolResponse.error / exit_code !== 0, augment crash-free behavior,
  cwd validation, corrupted/missing meta.json, unknown event names,
  empty stdin, and the no-.gitnexus deep-nested case.
- scripts/cross-platform-tests.ts: registers all three antigravity
  test files (unit in PLATFORM_LOGIC, two integration files in
  SPAWN_CLI) so Windows and macOS CI exercise them on every run.

* fix(antigravity): review fixes — dedup, silent-failure guard, type coercion, glob filter

- Delete mergeGeminiSettingsHooks (verbatim copy of mergeHooksJsonc),
  replace call site with the original
- Unify geminiHasGitnexusHook into hasGitnexusHook with commandFragment
  parameter; delete the duplicate
- Guard against silent adapter-copy failure: verify the adapter file
  exists before registering the AfterTool hook entry in settings.json;
  surface helper copy errors instead of swallowing
- Fix toolSucceeded type coercion: use Number() so string exit_code
  values from Gemini CLI are handled correctly
- Align glob tool extractPattern with Claude adapter's restrictive
  regex filter (/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/)
- Remove bounds-only toBeGreaterThan(0) assertion (DoD §2.7)
- Add antigravity adapter to HOOK_FILES windowsHide regression list

* chore(autofix): apply prettier + eslint fixes via /autofix command

* chore: trigger CI

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 14:46:17 +01:00
eddie.pan
5ce448a93a
feat(wiki): support local Claude and Codex providers (#1769)
* feat(wiki): support local Claude and Codex providers

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(wiki): address local CLI provider review findings

- Add subprocess timeout: LocalCLIConfig gains requestTimeoutMs,
  runLocalCLI sets a kill timer that rejects with an actionable error
  matching the HTTP timeout message format. --timeout is no longer
  silently ignored for claude/codex providers.
- Add windowsHide: true to spawn() to prevent console window flash on
  Windows, matching cursor-client.ts behavior.
- Skip GITNEXUS_MODEL env var for local providers so a user's OpenAI
  model name doesn't cross-contaminate claude/codex CLI invocations.
  Precedence for local providers: --model → savedLocalModel → ''.
- Guard against empty stdout: reject with actionable error when CLI
  exits 0 but produces no output, preventing silent empty wiki pages.

* fix(wiki): address deep-review findings in local CLI providers

- Move empty-output guard from runLocalCLI to per-provider callers so
  Codex can read --output-last-message file even when stdout is empty
- Merge existing config in interactive setup (local + Azure paths) to
  prevent saveCLIConfig from erasing previously saved API keys
- Use StringDecoder for stdout/stderr to handle multi-byte UTF-8 chars
  split across pipe chunk boundaries
- Distinguish ENOENT from non-zero exit in detectLocalCLI so users see
  auth guidance instead of misleading "CLI not found" when the binary
  exists but is not authenticated

* test(wiki): add subprocess contract tests for local CLI providers

Add 21 integration-level tests covering the Claude and Codex subprocess
contracts that wiki-flags.test.ts mocks out:

- Claude argv: -p, --output-format text, --no-session-persistence,
  --model conditional, stdin prompt content, CI=1, windowsHide:true
- Codex argv: exec subcommand, --sandbox read-only, -c approval_policy,
  --output-last-message temp path, --cd, stdin marker, --model
- Timeout: kill timer fires and rejects, no timer when unset
- Codex file fallback: stdout used when file missing, error when both empty
- detectLocalCLI: warn on non-ENOENT, silent on ENOENT
- onChunk: cumulative byte count forwarded

Also register the test in cross-platform-tests.ts SPAWN_CLI section and
fix detectLocalCLI ENOENT detection logic (invert the check so non-ENOENT
errors produce a warning).

* fix(wiki): platform-aware process tree kill and Codex contract snapshot

- Add killChildTree helper that uses taskkill /T /F /PID on Windows to
  terminate the entire process tree (including cmd.exe grandchildren),
  with fallback to child.kill() if taskkill fails or on non-Windows
- Add Codex CLI flag contract snapshot test that locks the exact spawn
  args — any flag rename, reorder, or removal is caught immediately
- Add Windows taskkill tests: success path asserts taskkill called with
  correct PID and /T /F flags, failure path verifies child.kill() fallback

---------

Co-authored-by: eddie.pan2 <eddie.pan2@jtexpress.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
2026-05-25 12:59:48 +01:00
ChamHerry
fc6007e70b
feat(i18n): make web and CLI language-aware (#1748) 2026-05-23 06:14:24 +01:00
Copilot
87b91c821e
fix(lbug): add WAL checkpoint-threshold control (#1772)
* Initial plan

* fix(analyze): add WAL auto-checkpoint CLI control and default-off behavior

* test(analyze): share lbug auto-checkpoint parsing and align validation

* fix(analyze): always enable lbug auto-checkpoint and expose threshold control

* refactor(lbug): inline always-on auto-checkpoint constructor arg

* fix(analyze): guide checkpoint-threshold on Ladybug WAL checkpoint IO failures

* test(analyze): cover checkpoint IO guidance and add integration guard

* fix(analyze): tighten checkpoint IO detection and remove test hook

* fix(analyze): remove checkpoint test hook and tighten error matching

* fix(analyze): rename to wal-checkpoint-threshold, raise default, add manual checkpoint driver with retry

Address review feedback on PR #1772:

- Rename CLI flag, env var, AnalyzeOptions field, recovery-hint tag, and
  parser/constants from lbug-* to engine-neutral wal-* (matches the existing
  WAL_RECOVERY_SUGGESTION / isWalCorruptionError convention).
- Raise default threshold from -1 (Ladybug stock ~16 MiB) to 64 MiB so users
  on the default config no longer hit the original rename/remove race.
- Align both READMEs to publish 67108864 (64 MiB) instead of 65536 (which
  would have made the crash more frequent).
- Add wal-checkpoint-driver.ts: a periodic manual CHECKPOINT driver wrapped
  in a 3-attempt jittered retry (50/200/500 ms), driven from runFullAnalysis.
  Opt-out via GITNEXUS_WAL_MANUAL_CHECKPOINT=0. Moves the race window into a
  JS-controllable retry surface while keeping native auto-checkpoint on.
- Move LBUG_CHECKPOINT_RENAME_RE / REMOVE_RE plus the predicate (renamed to
  isLbugCheckpointIoError) into lbug-config.ts alongside isWalCorruptionError.
  Predicate is now exported. Add a permissive fallback matcher and pin the
  matched Ladybug version in comments.
- Warn instead of silently defaulting when GITNEXUS_WAL_CHECKPOINT_THRESHOLD
  is set to a non-empty unparseable value (closes the CLI-vs-env asymmetry).
- Add a typed RecoveryHint string-literal union in cli-message.ts so future
  hint tags can't drift.
- Add a real integration test under test/integration/ that triggers a
  Ladybug checkpoint IO failure via a pre-existing directory at the rename
  target (portable across platforms; no test-only injection hook).
- Add small-disk / CI caveat (32 MiB secondary suggestion) to the recovery
  hint and README env-var rows.
- Document CLI/env precedence in the analyze --help block.
- Help placeholder: <value> -> <bytes>.
- Rename analyze-lbug-auto-checkpoint.test.ts to use the new wal-* token.

* chore(lbug): remove dead jitteredDelay helper and apply prettier

- Drop unused `jitteredDelay` function flagged by CodeQL in PR #1772; the
  retry loop already inlines the same calculation with the injectable
  `randomImpl` so the helper was dead. Move the non-cryptographic-by-design
  comment next to the actual jitter site.
- Apply `prettier --write` to wal-checkpoint-driver.ts and the new
  integration test to absorb the PR autofix bot's formatting findings.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
2026-05-22 14:46:49 +01:00
Copilot
c34c36036f
fix(workers): resilient + zero-copy ingestion worker pool — prevent analyze hangs on TS-root-scale loads (#1693)
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
* Initial plan

* fix: skip worker-timeout files in sequential fallback and optimize TS capture node lookup

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0e53743e-0600-4690-bd0d-198894daef58

* refactor: clarify TS capture helpers after validation feedback

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0e53743e-0600-4690-bd0d-198894daef58

* fix(workers): exclude in-flight file on worker error/exit, not just singleton timeout

WorkerPoolDispatchError previously surfaced the stalled path only for the
singleton-timeout final-fail branch. Worker `error` and `exit` events (and
the msg-channel `error` reply) fell back to plain `Error`, so the sequential
fallback re-attempted every file in the active job — re-hanging on the same
pathological file when the worker crashed mid-parse.

Lift the in-flight-file inference into `inFlightExcludePath(job, lastProgress)`
and wire it into the three remaining in-pool failure sites. `lastProgress` is
already in `runWorker` scope, so `items[lastProgress]` (the next file the
worker was about to acknowledge) is the best single guess at the culprit;
earlier files are still re-tried sequentially. Returns `[]` when no path is
determinable (`lastProgress >= items.length`, or path missing/non-string) so
sequential retries the whole job.

Replacement-worker startup failures stay plain `Error` (no job context); the
result-before-flush protocol bug stays plain `Error` (code fault, not file).

Tests cover the three new exclusion paths plus a negative test confirming
non-WorkerPoolDispatchError throws fall through to full sequential retry.

* fix(review): apply autofix feedback

- Use cause-neutral "worker-excluded" label in skip messages and tests now
  that worker error/exit paths share the same exclusion contract as
  singleton-timeout (correctness + maintainability reviewers).
- Add JSDoc to findSelfOrAncestorOfType{s} explaining the parent-walk
  short-circuit vs root-DFS fallback (maintainability reviewer).

* feat(workers): resilient + scalable worker pool

Restructures `createWorkerPool` so a single bad file no longer kills the
pool for the rest of an analyze run. Five interlocking layers:

1. **Auto-respawn on error/exit** — worker death triggers `replaceWorker`
   on the same slot, bounded by `maxRespawnsPerSlot` (default 3). The slot
   is dropped from rotation when the budget is exhausted; other slots
   keep running.

2. **Circuit breaker** — replaces the permanent `poolBroken=true` with a
   consecutive-failure counter. The pool only trips after
   `consecutiveFailureThreshold` deaths (default `max(3, poolSize)`) with
   no successful job in between. A successful job resets the counter so
   transient bursts of bad files don't escalate.

3. **Session-scoped file quarantine** — paths identified as the in-flight
   file at the moment of a worker death are added to a `Set<string>` on
   the pool. `dispatch()` filters quarantined items up front (they never
   reach a worker again this pool lifetime). Exposed via the new
   `WorkerPool.getQuarantinedPaths()` so callers can log/route them.
   `processParsing` surfaces the per-chunk quarantine summary alongside
   the existing fallback-exclusion log.

4. **Authoritative in-flight tracking** — `parse-worker.ts` emits
   `{type:'starting-file', path}` before each file. The pool tracks this
   per slot and uses it for crash attribution, falling back to the
   `items[lastProgress]` heuristic only when no starting-file has been
   observed (very-early crash, older worker build). Closes the
   reorder/race concerns raised by reviewers C1 and R3 in the earlier
   review run.

5. **Per-job cumulative timeout budget** — each `WorkerJob` tracks the
   total wall time spent across attempts/splits/retries. When the budget
   is exhausted (default 5x `subBatchIdleTimeoutMs`), the pool surfaces
   the in-flight path instead of letting exponential backoff balloon
   into multi-hour stalls.

Cross-layer wiring: a new `wakeIdleSlots` helper kicks any non-busy live
slot when items are requeued (after a death or split-retry), so a dropped
slot doesn't strand work in the queue. `recoverAndResume` consolidates
the per-job teardown shared by the three in-pool death sites (`error`,
`exit`, msg-channel `error`).

New env knobs: `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT`,
`GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS`,
`GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD`.
New `WorkerPoolOptions.workerFactory` injection point for unit tests.

Tests: 12 new unit tests using a FakeWorker mock cover quarantine
seeding, slot-respawn, slot-drop after budget, breaker trip + reset,
and quarantine filtering. Plus option-resolution tests for the three
new env vars. All 19 worker-pool/-fallback/-options tests pass; full
unit suite 6040 passed / 30 skipped / 0 failed.

* fix(workers): apply code-review fixes (12 findings)

Walks through every finding from ce-code-review run
20260519-094648-3549cf5e. All 12 picked Apply.

Critical:
- F1 — Layer 5 cumulative-timeout exhaustion no longer silently drops
  the rest of the job. `requeueRemainder` is now invoked before
  `handleWorkerDeath` in both Layer 5 and singleton-final-fail give-up
  paths so non-quarantined items get re-tried by another worker.
- F2 — idle-timer recovery overhaul. `!shouldContinue` branch no
  longer calls `replaceWorker` (double-spawn race with the
  `handleWorkerDeath` inside `requeueAfterTimeout`). `shouldContinue`
  branch now enforces `maxRespawnsPerSlot` before respawning, closing
  the budget-bypass for the timeout-retry path. Also fixes premature
  `maybeDone` by simplifying the bookkeeping.
- F3 — `requeueRemainder` no longer pre-charges `cumulativeTimeoutMs`
  by `job.timeoutMs`. The death itself consumed no budget, so the
  next `requeueAfterTimeout` was double-billing the first attempt.
- F4 — `WorkerPool.getQuarantinedPaths` is now optional on the
  interface, matching the defensive `?.()` call site and the existing
  mocks. Removes the contract-vs-callsite contradiction.
- F5 — per-job unattributed-death tracking. When a worker dies with
  no exclusion attribution, `requeueRemainder` tracks death count per
  `startIndex`. First time: re-queue intact. Second time: quarantine
  items[0] as best guess, or drop the job entirely when items lack
  paths. Bounds the death loop the original design admitted to.
- F6 — per-slot consecutive-failure counter. Replaces the pool-wide
  scalar so a chronically-failing slot trips the breaker on its own
  streak instead of being masked by another slot's successes.

Smaller:
- F7 — exhaustiveness `never` check on `WorkerOutgoingMessage` union.
- F8 — recursive `runWorker` on fully-quarantined jobs converted to
  a while-loop.
- F9 — `tripBreaker` calls `reject(err)` BEFORE awaiting
  `worker.terminate()`. A stuck terminate no longer blocks the caller.
- F10 — `parsing-processor.ts` quarantine log de-duplicates per pool
  instance via a `WeakMap`. Only newly-quarantined paths are logged
  in each chunk; the per-chunk count still surfaces via progress.
- F11 — extract `firstPath` local in `requeueAfterTimeout`; eliminates
  double `itemPath` call and the `unknown as string` cast.

Tests (F12, 6 new):
- crash-error event path (errorHandler).
- F5 drop-branch coverage via items without `.path`.
- Common-case unattributable crash falling back to items[0] heuristic.
- `replaceWorker` startup failure (workerFactory emits 'exit' before
  'online').
- All-slots-dropped breaker trip.
- `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` env override.

Residual gap (deferred): no unit test exercises the Layer 5
cumulative-budget runtime path — requires fake-timer interleaving
with FakeWorker that's too brittle for this iteration. Tracked.

Unit suite: 257 files / 6056 passed / 30 skipped / 0 failed.

* test(workers): integration tests for resilience layers + fix requeue-after-timeout flow

Adds 6 new real-worker integration tests covering the PR #1693
resilience layers + fixes 3 follow-on bugs surfaced while writing them.

New integration coverage (real worker threads + temp fixture scripts):

- `respawns the slot after worker process.exit and finishes the work on
  the replacement` — exercises Layer 1 auto-respawn + Layer 3 quarantine
  through real IPC.
- `attributes exactly via authoritative starting-file message on worker
  crash` — Layer 4 end-to-end: starting-file message → exact quarantine
  attribution (not the items[0] heuristic).
- `quarantine filters subsequent dispatches without sending to a worker`
  — second dispatch's sub-batch payload audited via filesystem; the
  quarantined path is never sent across the message channel.
- `drops a slot after maxRespawnsPerSlot and continues on the survivor`
  — 2-slot pool, slot dies twice past budget, survivor finishes
  re-queued remainder.
- `trips the circuit breaker on cascading per-slot consecutive failures`
  — single-slot pool, dies on every job, breaker trips after
  consecutiveFailureThreshold with WorkerPoolDispatchError carrying
  the cumulative quarantine.
- `survives a worker error event (uncaught throw) the same as a
  process.exit` — validates recoverAndResume on the errorHandler path
  via a real worker `throw` (not just process.exit).

Bug fixes uncovered while writing these tests:

1. **Stack-overflow recursion in runWorker's no-worker branch** —
   `if (!worker) { ...; wakeIdleSlots(); maybeDone(); }` recursed
   indefinitely when multiple slots were mid-respawn simultaneously
   (wakeIdleSlots → runWorker → no worker → wakeIdleSlots → …).
   Removed the wakeIdleSlots call: the slot's own respawn IIFE owns
   runWorker post-respawn, and other slots will pick up work via
   finishJob's runWorker.

2. **requeueAfterTimeout dispatched work before respawn completed** —
   the F2 fix had `requeueAfterTimeout` `void`-discarding
   `handleWorkerDeath`, so the `!shouldContinue` IIFE had no way to
   know when the respawn finished. New design: `requeueAfterTimeout`
   returns a `TimeoutDecision` discriminated union; the IIFE owns
   the death-and-respawn-and-dispatch orchestration in an async
   closure so it can `await handleWorkerDeath` and then call
   `runWorker` deterministically.

3. **Stalled-singleton + protocol-error + replacement-startup-crash
   tests** had stale contracts predating the resilience refactor. The
   stalled-singleton no longer rejects (it quarantines + resolves
   `[]`); the protocol-error rejection message now mentions
   "circuit breaker tripped"; the replacement-startup-crash test
   documents the known `waitForWorkerOnline` race (online fires
   before the worker's main script runs, so a top-level throw looks
   like a successful spawn) — the test asserts the file is
   quarantined via the second-idle-timeout give-up path.

Full suite: 334 files / 8982 passed / 43 skipped / 0 failed (second
run; first run had a Vitest-reported flake from an uncaught worker
exception bleeding into the test report — repeated runs are clean).

* perf(workers): raise pool cap to cores-1 + defer per-chunk extraction to keep workers busy

User reported 4-5% CPU utilization on a multi-core machine during
ingestion. Two structural reasons:

1. **Pool cap.** `createWorkerPool` resolved size as
   `Math.min(8, max(1, os.cpus().length - 1))` — a 16-core box got 8
   workers (50% theoretical max). U1 lifts the default to
   `min(16, max(1, cores - 1))`, exposes `GITNEXUS_WORKER_POOL_SIZE`
   env override, and adds `--workers <N>` CLI flag (`0` disables the
   pool for sequential fallback).

2. **Per-chunk extraction serialized the loop.** Per chunk:
   dispatch → await workers → main-thread `processImportsFromExtracted`
   + `processHeritageFromExtracted` + `processRoutesFromExtracted`
   + `synthesizeWildcardImportBindings` + `seedCrossFileReceiverTypes`
   → next chunk dispatch. Workers sat idle through every extraction
   block. U2 (revised from the plan's pipelined-chunks design) defers
   these passes to a single end-of-loop batch. Chunk loop becomes
   parse + merge + accumulate. Resolution sees strictly-more-info
   (full repo graph) so cross-chunk import/heritage targets resolve at
   least as well as before. Memory cost: `deferredWorkerImports`
   accumulates across chunks; bounded by total file count, acceptable.

Plan deviation note: the plan called for an in-flight chunk pipeline
(N concurrent dispatches with bounded memory). That design needed
either a `processParsing` API refactor or duplicating its catch-block
fallback in `parse-impl`. The deferred-extraction approach delivers
the same "workers stay busy" outcome with much smaller surface area
and zero changes to `processParsing`. The `GITNEXUS_PARSE_CHUNK_CONCURRENCY`
env var documented in U2 of the plan is therefore not implemented in
this commit; if memory growth from `deferredWorkerImports` becomes
a problem at very-large-repo scale, a bounded sliding-window variant
can land as a follow-up.

Tests:
- New `test/unit/analyze-worker-pool-size.test.ts` covers --workers
  validation (5 invalid inputs rejected with exit code 1 + clear
  error; valid integers set the env var; `--workers 0` routes to
  sequential).
- Extended `worker-pool-resilience.test.ts` with `resolveAutoPoolSize`
  scenarios: env override, env=0, env above cap, invalid env fallback,
  auto-formula match, integer return type.
- Full unit suite: 6097 / 6127 passed / 30 skipped / 0 failed.
- Full integration suite (second run): 77 / 78 passed / 1 skipped /
  0 failed. First run had a known cosmetic flake from an uncaught
  worker exception bleeding into the test reporter.

Resilience contract from PR #1693 preserved: per-slot respawn budget,
circuit breaker, quarantine, authoritative in-flight tracking,
cumulative timeout budget — all unchanged.

New env vars surfaced in --help: GITNEXUS_WORKER_POOL_SIZE,
GITNEXUS_PARSE_CHUNK_CONCURRENCY (reserved for future bounded
pipelining).

* docs(readme): document --workers CLI flag

* feat(workers): add getStats() and per-chunk throughput logging

* test(workers): cleanup leaked temp-dirs and drop duplicate option-resolution block

- Add afterEach to worker-pool-resilience.test.ts cleaning up the per-test temp
  directory created by beforeEach (~25 stale dirs per CI run previously).
- Delete the duplicated describe('worker pool option resolution', ...) block.
  Verified the first block (lines 490-532) is a strict superset (includes the
  GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS env test the second block omitted),
  so deletion loses no test coverage.

Addresses PR #1693 review findings L2 (temp-dir leak) and L3 (duplicate block).

* feat(cli): thread --workers via PipelineOptions + snapshot/restore CLI env

Resolves PR #1693 review B2 (env-var leak in long-running hosts):

- --workers is now threaded through AnalyzeOptions -> runFullAnalysis
  -> PipelineOptions.workerPoolSize -> createWorkerPool's explicit
  poolSize arg, bypassing the GITNEXUS_WORKER_POOL_SIZE env channel.
  The env var remains as a back-compat fallback inside resolveAutoPoolSize
  for operators who set it directly.
- analyzeCommand and wikiCommand snapshot the GITNEXUS_* env vars they
  mutate at function entry and restore them in finally. Inner *Impl
  extraction keeps the diff surgical (no body re-indent). process.exit(0)
  on the CLI success path still terminates the process; restoration
  matters for programmatic callers (tests, long-running hosts) reaching
  early-return paths or the alreadyUpToDate fast path.
- Tests updated to assert the new behavior:
    analyze-worker-pool-size.test.ts: workerPoolSize flows through
      runFullAnalysis options; env is not mutated; back-to-back calls
      see their own values, not the previous call's leak.
    analyze-worker-timeout.test.ts: env IS set during the runFullAnalysis
      call (captured via mockImplementation) and restored after, proving
      the timeout reaches downstream while the leak fix holds.
- Also addresses L4: afterEach NODE_OPTIONS restore so back-to-back test
  runs don't accumulate --max-old-space-size=8192 tokens.

Addresses PR #1693 review B2 (blocker) and L4 (test polish).

* feat(workers): harden worker lifecycle (messageerror + availableParallelism + ready handshake)

Resolves PR #1693 review H1, H2, M4:

H1 - messageerror handler at every dispatch site
  V8 deserialization failure on postMessage previously left the message
  silently lost; the pool would wait out the idle timeout (default 30s)
  instead of treating it as worker death. The dispatch loop now wires
  worker.once('messageerror', ...) alongside error/exit and routes through
  recoverAndResume so the existing per-slot respawn budget, in-flight
  file attribution, and circuit-breaker layers fire as designed.

H2 - resolveAutoPoolSize uses os.availableParallelism()
  Mirrors the pattern at capabilities.ts:85 (defaultEmbeddingThreads).
  os.cpus().length returns the host CPU count, which over-sizes the pool
  on cgroup-limited containers, taskset-restricted runtimes, and CI
  runners with explicit CPU quotas. Falls back to os.cpus().length on
  Node < 18.14.

M4 - worker-side ready handshake replaces online-trust
  parse-worker.ts now emits {type: 'ready'} after all top-of-script
  initialization completes, BEFORE the message handler is attached. The
  pool's renamed waitForWorkerReady listens for this message under a
  bounded WORKER_READY_TIMEOUT_MS (5s) budget instead of trusting Node's
  online event - which fires when the worker thread starts, BEFORE the
  script body runs, letting init crashes slip past pool startup. ready
  is added to WorkerOutgoingMessage with an exhaustiveness-checked
  no-op branch in the dispatch handler (defensive: the message is
  consumed by waitForWorkerReady before dispatch handlers attach).
  messageerror is wired into waitForWorkerReady the same way.

Test scaffolding:
  - FakeWorker emits {type: 'ready'} in addition to 'online' so
    replacement workers in unit tests don't hit the 5s budget.
  - Integration test ad-hoc worker scripts go through a writeReadyWorker
    helper that prepends the ready handshake. Tests intending to script
    "crash BEFORE ready" can bypass the helper.

61/61 worker-pool unit tests pass; 28/28 integration tests pass.

* feat(parse-impl): monotonic progress + verbose-gated throughput log + seed-before-build

Resolves PR #1693 review M2, M3, L1, L5 in a single parse-impl.ts pass:

M2 - Monotonic progress through deferred phase (no more "stuck at 82%")
  Previously the deferred resolution stages (imports, heritage, routes,
  calls) all emitted percent: 82 — the UI looked frozen for the duration
  of the deferred work, which on large repos is several seconds to minutes
  and visually identical to the hang PR #1693 set out to fix.
  Redistributed:
    parse phase:  20-70 (was 20-82)
    imports:      70-75
    heritage:     75-80
    routes:       80-85
    calls:        85-95
  Each deferred stage now advances through its own band via the existing
  per-batch progress callback. Skipped stages (zero deferred input) leave
  their band as a no-op jump - the next stage still starts at its own
  band, preserving strict monotonicity. The "no parseable files" early
  return now jumps to 95 (was 82), and the duplicate "Parsing N files..."
  announcement is suppressed when totalParseable === 0 to avoid a
  non-monotonic 95 -> 20 regression that pre-existed (uncovered by the
  new monotonic test).

M3 - Throughput log gated on `--verbose`, not just NODE_ENV=development
  The per-chunk files/s log was gated on `isDev`, so operators running
  `gitnexus analyze --verbose` in a production install never saw it.
  Now fires when (isDev || isVerboseIngestionEnabled()) — matches the
  documented promise that `--verbose` shows tuning observability.

L1 - Typo rename: `chunkChunkStartMs` -> `chunkStartMs`

L5 - `buildExportedTypeMapFromGraph` runs BEFORE `seedCrossFileReceiverTypes`
  Previously the seeding branch was reached with `exportedTypeMap.size === 0`
  in the worker path (the map was only built far below, AFTER the seeding
  branch), so the seed dead-coded itself silently and call resolution
  never got the cross-file receiver-type enrichment. Now the map is
  populated from the in-progress graph before the seed call; the
  post-parse builder remains as a defensive sequential-path fallback,
  guarded by `size === 0` so we don't pay the cost twice on the worker
  path. Net win: cross-file CALLS edges that previously had no receiver
  type now get enriched.

New test: parse-impl-progress-monotonic.test.ts
  Asserts the emitted percent stream is strictly non-decreasing across
  the parse + deferred phases, and that the deferred band (>=70) is
  actually reached. Also pins the "no parseable files" path to exactly
  [95] so the 95 -> 20 regression we just fixed can't re-emerge.

* feat(parse-impl): bounded chunk concurrency via file-pre-fetch pipeline

Resolves PR #1693 review B1 (GITNEXUS_PARSE_CHUNK_CONCURRENCY documented
in --help but unimplemented).

The chunk loop now pre-fetches chunk file contents up to
`parseChunkConcurrency` chunks ahead of the worker-dispatch cursor so
disk I/O overlaps with worker compute. Worker dispatch itself stays
serial because WorkerPool.dispatch is not reentrant — concurrent calls
would race on the shared per-slot busy/in-flight state, regressing the
hang/resilience work this PR is built on. The pre-fetch path is the
honest interpretation of "concurrent in-flight parse chunks" that the
help text advertises: I/O overlap, not parallel worker dispatch.

Concurrency value resolution:
  1. PipelineOptions.parseChunkConcurrency (threaded from CLI)
  2. GITNEXUS_PARSE_CHUNK_CONCURRENCY env var
  3. Default 2 (matches the help text)

F4 (wildcard-synthesis ordering) is preserved: deferred-state
aggregation runs in chunkIdx order because the for-loop iterates
sequentially after awaiting each chunk's pre-fetched contents.
Cross-chunk processors (processImportsFromExtracted,
synthesizeWildcardImportBindings, etc.) still run only after all
chunks complete — they see deterministic input regardless of
file-read completion order.

Concurrency=1 produces behavior identical to the pure-serial loop;
that's the regression baseline.

New test: parse-impl-chunk-concurrency.test.ts
  - Asserts graph output is identical (nodeCount + relationshipCount)
    between parseChunkConcurrency=1 and =2 — the critical correctness
    invariant. Exact .toBe(N) comparisons per DoD §2.7 (the second run's
    counts must equal the first run's exactly).
  - Pins specific fixture symbols (foo/bar/Baz) under both
    parseChunkConcurrency=1 and the env-fallback (3) path.
  - Env-fallback test confirms GITNEXUS_PARSE_CHUNK_CONCURRENCY is
    honored when the option is undefined.

* test(workers): pin cumulative-timeout exhaustion behavior

Resolves PR #1693 review M6: the existing resilience suite asserts only
the *default value* of maxCumulativeTimeoutMs (5x subBatchIdleTimeoutMs),
not that dispatch actually aborts the offending job when the cumulative
wall-clock budget is exhausted. Without this test, a future refactor
could remove the exhaustion branch in requeueAfterTimeout and the suite
would stay green while the pool sat in retry loops for an hour on a
real production stall.

Scenario:
  subBatchIdleTimeoutMs    = 100ms
  timeoutBackoffFactor     = 10
  maxCumulativeTimeoutMs   = 300ms

Single file, HangingWorker that never responds. First attempt times
out at 100ms (cumulative=100). The next backoff (1000ms, cumulative
1100ms) exceeds the 300ms cap, so requeueAfterTimeout returns
give-up on the first timeout retry and the file goes to the session
quarantine. Asserts:
  - pool.getQuarantinedPaths() includes 'src/stuck.ts' after dispatch
  - if dispatch rejected, the error is a WorkerPoolDispatchError
    (the typed surface that routes to sequential fallback)

Uses a local minimal HangingWorker double rather than the full
action-scripted FakeWorker from worker-pool-resilience.test.ts —
the inverse pattern (always hang) doesn't need the scripted-action
machinery and keeps the test file focused on the one behavior.

* docs(readme): add environment-variables reference table

Resolves PR #1693 review L6: operator-facing env vars were either
mentioned inline (GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS) or only
documented via `gitnexus --help`, with no single place to look up
the full set. The new "Environment variables" subsection under the
Quick Start CLI block lists every operator-facing knob with default,
effect, and tuning guidance, matching the names in cli/index.ts
addHelpText post-U2 / U1.

Covers:
  GITNEXUS_WORKER_POOL_SIZE           (--workers)
  GITNEXUS_PARSE_CHUNK_CONCURRENCY    (newly real per U1)
  GITNEXUS_VERBOSE                    (--verbose)
  GITNEXUS_MAX_FILE_SIZE              (--max-file-size)
  GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS (--worker-timeout × 1000)
  GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES
  GITNEXUS_CHUNK_BYTE_BUDGET
  GITNEXUS_NO_GITIGNORE
  GITNEXUS_SKIP_OPTIONAL_GRAMMARS

CLI flag vs env-var precedence is stated explicitly (CLI > env > default)
so operators running long-lived hosts (MCP server, eval-server) know
which channel wins.

* test(workers): pin quarantine path round-trip and non-normalization contract

Resolves PR #1693 review M5 (Windows quarantine path-normalization
coverage). worker-pool.ts quarantines paths via a Set<string> keyed by
exact string equality. The existing suite never asserted this contract,
which lets a future "helpfully normalizing" refactor on one side of the
pipeline (caller, worker, or pool) silently break quarantine filtering
on Windows.

This file pins the contract from both directions:

1. Round-trip: a path the caller dispatches with backslashes
   (src\bad.ts) flows through starting-file -> death -> quarantine ->
   next-dispatch filter verbatim. The replacement worker never sees the
   re-dispatched bad path because the pool's pre-dispatch filter
   short-circuits it.

2. Non-normalization: quarantining src\poison.ts does NOT filter
   src/poison.ts. Whoever changes that contract has to update this test
   alongside (the load-bearing assertion catches accidental
   path.normalize() calls in the quarantine path).

Runs on every platform — the path strings are test-injected, so the
test exercises the same code path regardless of the host's path.sep.
Used a self-contained FakeWorker that emits {type:'ready'} for U3's
waitForWorkerReady handshake, so the test doesn't depend on the larger
worker-pool-resilience.test.ts harness.

* test(typescript): pin capture-anchor rewrite invariants (B5 regression)

Resolves PR #1693 review B5: the captures.ts ancestor-walk rewrite
(findSelfOrAncestorOfType[s] + pickFirstNode replacing the prior
findNodeAtRange-from-root path) was semantically equivalent to its
predecessor per Lane 4 of the production-readiness review, but the
existing typescript-captures.test.ts didn't pin the specific sharp
edges where an over-aggressive walk would silently break captures.
This file does.

Each test exercises a capture class whose anchor type is one the
rewrite explicitly handles:

  - member call obj.foo() -> @reference.call.member (call_expression
    anchor walks to self)
  - dynamic import import("./helper") -> raw @import.dynamic gets
    decomposed by splitImportStatement into @import.statement with
    @import.kind=dynamic + @import.source stripped of quotes
  - JSX <Foo /> in .tsx -> @reference.call.free emitted (TSX query
    pattern, query.ts:899-905) but @declaration.parameter-count is
    NOT synthesized because findSelfOrAncestorOfType('call_expression')
    returns null on a jsx_self_closing_element anchor. Pre-rewrite the
    range lookup also returned null. Pinning this contract catches
    accidental "walk JSX -> outer call" refactors.
  - constructor `new Foo(1,2)` -> @reference.call.constructor (new_expression
    anchor walks to self)
  - named/namespace import + re-export -> @import.statement (one each)
  - class method override -> @declaration.method per class, no collapse
  - member read obj.foo (no call) -> @reference.read.member

All assertions use exact .toBe(N) per DoD §2.7.

* test(parse-impl): pin multi-chunk graph equivalence under deferred extraction

Resolves PR #1693 review B4: the deferred-extraction reorder (moving
processImportsFromExtracted / Heritage / Routes / Wildcard /
ReceiverTypes from per-chunk to end-of-loop) was proven observably
equivalent by Lane 4 of the production-readiness review. Until now,
the existing suite never asserted cross-chunk graph equivalence,
which lets a future refactor that accidentally tightens the per-chunk
vs end-of-loop coupling silently break cross-chunk resolution.

This test forces multi-chunk parsing on a small fixture by setting
GITNEXUS_CHUNK_BYTE_BUDGET=64 BEFORE the parse-impl module loads
(the budget is captured at module load via vi.resetModules — a future
move to function-scope env reads is U14 in Phase 2). Then runs the
same fixture under a 10MB budget (single chunk) and asserts the two
graphs are byte-identical: same nodeCount, same relationshipCount,
exact .toBe(N) per DoD §2.7.

Fixture: 3-file class hierarchy with cross-file inheritance — Animal
(a.ts) -> Dog extends Animal (b.ts) -> makeDog returns Dog (c.ts).
Forces the resolver to chain imports + heritage across chunks. A
second test pins specific symbol names (Animal, Dog, makeDog, speak,
bark) in the multi-chunk graph so a regression in chunk-boundary
resolution surfaces as a missing-symbol failure with a specific
diagnostic instead of a bare count mismatch.

* test(parse-impl): wall-clock integration pinning multi-chunk pipeline (B3)

Resolves PR #1693 review B3 — the final P0/P1 merge blocker. With this
test, all five doc-review blockers (B1-B5) are pinned by regression
coverage.

The PR's headline claim is "analyze no longer hangs on TS-root-shaped
loads". The existing suite pins each resilience layer (worker-pool-
resilience.test.ts), the deferred-extraction equivalence (U7), and
the chunk-concurrency contract (U1). What was missing: a single
end-to-end run that exercises the full chunked parse-and-resolve
path on a multi-chunk fixture, BOUNDED by a wall-clock budget so a
regression that re-introduces the hang fails this test loudly via
timeout rather than slipping past as a count drift.

Implementation:
  - 17-file synthetic fixture: 15 small modules (one function each),
    one "realistic dense" complex.ts (30 functions + class + interface),
    and an index.ts re-exporting them. Forces cross-chunk import
    chains.
  - GITNEXUS_CHUNK_BYTE_BUDGET=64 via vi.resetModules forces multi-chunk
    parsing on the small fixture.
  - Promise.race with 30s timeout: a hang fails as
    "exceeded WALL_CLOCK_BUDGET_MS — likely the hang B3 was meant to
    prevent", not as a bounds-only inequality (DoD §2.7 distinction —
    hang-detector via exception, not regression-mask via inequality).
  - Exact .toBe(true) assertions on specific expected symbols
    (fn0..fn14, Service, Config, configure, describe, complex0/15/29)
    so a silent mid-chunk crash that exits 0 without producing graph
    data also fails this test, not just the hang case.

Scope: runs the sequential-fallback path (skipWorkers: true) because
the full real-worker scenario requires a built dist/parse-worker.js
and ~60s wall-clock per run — appropriate for a CI-integration job,
not vitest. The load-bearing invariants pinned here catch the bulk
of B3's concern; the dist-worker swap is a Phase 2 follow-up
documented in the file header.

* refactor(parse-impl): move chunk-byte-budget env read to function scope

Resolves PR #1693 review F7 / U14: pre-U14, `CHUNK_BYTE_BUDGET` was a
module-load IIFE constant that captured `GITNEXUS_CHUNK_BYTE_BUDGET`
once and froze the value for the module's lifetime. That defeated
per-call option threading (a future
`PipelineOptions.chunkByteBudget` was silently no-op'd because the
function body read the frozen module-level constant) AND forced tests
to use `vi.resetModules` to vary chunk layout. The U7
deferred-extraction test and the U6 multi-chunk integration test
both used the workaround.

After this change:

  - `DEFAULT_CHUNK_BYTE_BUDGET = 2 * 1024 * 1024` stays as a
    module-level constant — purely a default, no env access.
  - `resolveChunkByteBudget(options)` runs per call: option wins,
    then env, then default. Same options-first/env-fallback/default
    pattern as resolveAutoPoolSize and the U1 parseChunkConcurrency
    resolver — keeps the ingestion code's configuration model uniform.
  - `PipelineOptions.chunkByteBudget?` added with documentation that
    threading through options lets long-running hosts (eval-server,
    MCP daemon) size per-call without leaking process.env state
    across analyze invocations.

New test (parse-impl-env-reads.test.ts) pins all four behaviors:
  1. option-first: option present + env present -> option wins
  2. env-fallback: option absent + env present -> env wins
  3. default-fallback: both absent -> 2 MB default
  4. per-call: two back-to-back runs in the same vitest worker with
     different chunkByteBudget option values observe their OWN values,
     proving the module-load freeze is gone (no vi.resetModules in
     this test — that's the invariant being verified).

All four assertions use exact `.toBe(N)` per DoD §2.7. The chunk
count is observed by parsing the `Parsing chunk X/Y` progress message
stream — a stable proxy that doesn't require exposing internal
parse-impl counter state.

Note: U7 and U6 tests still use `vi.resetModules` because they were
written before this change. A follow-up cleanup could simplify those
tests (drop the resetModules dance, pass chunkByteBudget via options),
but they pass as-is so this commit doesn't touch them.

* feat(workers): per-slot generation counter for late-event protection (U12)

Adds a monotonic per-slot generation counter to createWorkerPool's
state. Each successful worker replacement (replaceWorker) bumps the
slot's counter exactly once — atomically with the workers[slotIndex]
swap, so observers (getStats) see the new (worker, generation) pair
consistently. Handler closures in the dispatch loop capture the
slot's generation at attach time and short-circuit when they fire
on a stale generation.

In the current implementation, cleanup() synchronously removes
listeners on a Worker instance the moment a death is observed, so
no listener naturally fires on a stale generation — the guard is a
defensive layer protecting against any future refactor that loosens
cleanup() ordering or re-attaches handlers across the swap. The
load-bearing observable is the slotGenerations[] array exposed via
WorkerPoolStats so operators (and tests) can confirm a slot was
actually replaced and not just the same worker recycled.

Implementation:
  - const slotGenerations: number[] = new Array(size).fill(0) in
    createWorkerPool's per-pool state, alongside respawnCount and
    consecutiveFailuresPerSlot.
  - replaceWorker: slotGenerations[workerIndex]++ AFTER the
    workers[workerIndex] = replacement swap (only on the success
    branch — drop-slot paths leave the counter unchanged).
  - runWorker dispatch loop: const slotGen = slotGenerations[workerIndex]
    captured before handler attachment; every handler (handler /
    errorHandler / exitHandler / messageErrorHandler) starts with
    `if (slotGenerations[workerIndex] !== slotGen) return`.
  - WorkerPoolStats gains `readonly slotGenerations: readonly number[]`.
  - getStats() returns slotGenerations.slice() so callers can't mutate
    pool state by writing to the returned array.

Two existing toEqual snapshots in worker-pool-resilience.test.ts
extended with the new slotGenerations field (both expect all-zeros —
neither test scenario triggers a respawn).

New test file (worker-pool-slot-generation.test.ts, 4 tests):
  1. Fresh pool: every slot at generation 0.
  2. Successful crash + respawn: generation bumps to 1 exactly once.
  3. Crash that drops the slot (maxRespawnsPerSlot:0): generation
     stays at 0 because no successful respawn happened. The dispatch
     rejection on breaker trip is the expected outcome here; the
     load-bearing assertion is the post-rejection stats.
  4. Multi-slot independence: one slot crashing bumps only that
     slot's generation, not the other. Order-independent via sort()
     because the round-robin assignment isn't pinned by contract.

All assertions exact .toEqual / .toBe per DoD §2.7.

* docs(bench): add parse-throughput benchmark scaffold (R13)

Resolves PR #1693 review R13 (benchmark artifact requirement).

Creates `gitnexus/bench/parse-throughput.md` documenting:

- Synthetic fixture spec (same shape as the U6 integration test, so
  CI smoke baseline and ad-hoc benchmark exercise the same paths).
- What to measure (wall-clock, peak heap, chunk count, getStats
  snapshot) and the hardware-shape metadata to record alongside.
- Harness recipe — vitest + env-var overrides to exercise sequential
  fallback vs worker-pool paths.
- Latest-measurement table with placeholder rows for the three paths
  (sequential, workers+concurrency, workers single-threaded) and an
  explicit "Status: scaffold — fill in before merging" callout. The
  U6 test's observed ~6 s wall-clock is captured as a smoke-baseline.
- Operator-tuning quick reference cross-linked to the README env-var
  section (U11) so the doc is actionable without re-reading the PR.
- "What this benchmark does NOT measure" section explicitly scoping
  the artifact's limits (synthetic ≠ real-repo, throughput-only ≠
  resilience-tested, Phase 3 IPC repack row reserved for U16-U17).

Mitigates the doc-review SG5 "static doc drift" concern via:
  1. Explicit "regenerate this file before merging" callout at the top.
  2. Self-contained methodology so anyone can re-run the numbers.
  3. Cross-links to the U6 integration test that already bounds the
     wall-clock as part of the CI suite — so "is it still completing?"
     is regression-tested even if the numbers in this doc drift.

The standalone harness script (`bench/scripts/parse-throughput.ts`)
remains a stretch goal per the original plan. The U6 vitest with
verbose ingestion logs covers the primary observability gap until
the standalone harness lands.

* perf(parse-impl): free deferred-extraction arrays after consumption (U15 lightweight M1)

PR #1693 review M1 noted that the deferred-extraction accumulator
arrays (`deferredWorkerImports`, `deferredWorkerCalls`,
`deferredWorkerHeritage`, `deferredConstructorBindings`,
`deferredAssignments`) were retained until function return, making
peak accumulator memory O(repo) instead of O(in-flight stage).

This commit implements the LIGHTWEIGHT version: free each array
immediately after its last consumer drains/reads it, dropping peak
accumulator memory progressively through the deferred-extraction
stages. The structural per-chunk streaming variant (the original
U15 framing) is deliberately deferred — the doc-review's adversarial
reviewer (A4) flagged it as defending unmeasured memory pressure,
and the simpler array-clearing captures the bulk of the benefit
without committing to a scheduling-strategy decision (microtask vs
parallel extractor task vs worker-side) that profile data should
inform.

Clears added:

  1. After `processImportsFromExtracted` (the sole consumer of
     `deferredWorkerImports`): clear the imports array before
     the heavier heritage/calls stages run.
  2. After `buildHeritageMap` (the LAST consumer of the raw
     `deferredWorkerHeritage` records — processCallsFromExtracted
     reads from the derived `fullWorkerHeritageMap` instead):
     clear the heritage array before the call-resolution stage.
  3. After `processAssignmentsFromExtracted` (the joint last
     consumer with processCallsFromExtracted for the calls/
     bindings/assignments triple): clear all three before
     downstream graph-build / scope-resolution uses its own
     working memory.

Arrays returned in the function result object (allFetchCalls,
allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries,
allParsedFiles) intentionally stay live — downstream consumers
need them.

Graph-output equivalence is preserved (U7 multi-chunk equivalence
test passes — the clears happen AFTER each array's last consumer
has copied data into the graph or derived structures).

* feat(workers): introduce protocol.ts wire-format module (U16, IPC scaffold)

Defines the binary frame for worker-thread IPC as an isolated, fully-tested
module. Production wiring is deferred to U17 — shipping the wire-format
contract first de-risks the migration by establishing a single source of
truth for the byte layout. Resolves the scaffold half of PR #1693 review
R12.

Wire layout (per message, single buffer):

  +---------+-----------+---------------------+
  | tag     | length    | payload bytes …     |
  | 1 byte  | 4 bytes   |                     |
  +---------+-----------+---------------------+

  tag    : MessageTag enum value (0x01 DispatchJob ... 0x08 Ready)
  length : little-endian uint32 byte count for the payload region
  payload: UTF-8 JSON-encoded value, possibly "null"

Why JSON for the body (rather than per-shape binary encoders): the
doc-review adversarial reviewer (A2) flagged that a true per-shape
binary encoder for the result message — which carries nested
heterogeneous extracted-call / import / heritage / route arrays —
would be 500-1500 LOC and a substantial maintenance burden. The
honest perf win the IPC repack targets is moving file CONTENTS via
ArrayBuffer transferList (zero-copy ownership transfer for the
largest single piece of state in any message). That win is captured
by U17 layering transferList over the bulk file-content payload while
keeping this module's framing for the surrounding metadata. If U18
benchmark data shows the JSON body is itself a bottleneck after U17
lands, a follow-up unit can swap to per-shape binary encoding behind
the same encodeMessage / decodeMessage surface without changing the
frame.

API:
  - MessageTag (const object): stable byte tags 0x01..0x08
  - PROTOCOL_HEADER_BYTES = 5
  - ProtocolDecodeError extends Error: distinct class so U17's
    pool-side handler can route protocol violations through the
    existing messageerror recovery layer (U3 H1) distinctly from
    other failure classes
  - encodeMessage(tag, payload): Buffer
  - decodeMessage(buf): { tag, payload }
  - Uses Buffer#subarray instead of the deprecated Buffer#slice

Tests (18, all exact-equality per DoD §2.7):
  - byte layout (tag at offset 0, length LE uint32 at offset 1)
  - empty/null payload encodes to 5-byte header + 4-byte "null" body
  - round-trip for every MessageTag with representative payloads
  - non-ASCII path string (UTF-8 byte-length boundary)
  - 9 MB payload (well past the existing 8 MB sub-batch budget)
  - decode errors surface as ProtocolDecodeError, not generic Error:
      * buffer < header size
      * tag outside valid range
      * declared length exceeds buffer
      * payload bytes are not valid JSON
  - error class name is preserved through prototype chain so callers
    can `err instanceof ProtocolDecodeError` reliably

* refactor(workers): extract quarantine into its own module (U13 partial)

Honest partial U13: extract the quarantine resilience layer (Layer 3
of the 5-layer model) into a dedicated module with a small explicit
interface. The full 5-module split that the original plan named was
flagged by doc-review A10 as abstraction-without-multi-consumer-demand
("Each has exactly one consumer: worker-pool.ts. None of these layers
is imported elsewhere in the codebase pre-extraction, and the plan
doesn't identify any future consumer.") This commit ships the smallest
self-contained layer as a named module to validate the factory +
interface pattern with minimal risk. The remaining four layers
(respawn-budget, cumulative-timeout, circuit-breaker, slot-attribution)
stay inline until a real second consumer emerges (e.g., a non-parse
worker pool that reuses the same resilience layers).

Module shape (`workers/quarantine.ts`, ~30 LOC):

  interface Quarantine {
    add(path: string): void;
    has(path: string): boolean;
    snapshot(): string[];   // defensive copy
    readonly size: number;  // getter, reflects state at access time
  }
  function createQuarantine(): Quarantine

Replaces in `worker-pool.ts`:
  - `const quarantined: Set<string> = new Set()` -> `createQuarantine()`
  - `quarantined.has(p)`            -> `quarantine.has(p)` (2 sites)
  - `quarantined.add(p)`            -> `quarantine.add(p)` (2 sites)
  - `quarantined.size`              -> `quarantine.size` (2 sites)
  - `Array.from(quarantined)`       -> `quarantine.snapshot()` (6 sites)

Public worker-pool.ts API is unchanged — `getQuarantinedPaths()` still
returns the same defensive `string[]` copy. The behavioral contract is
preserved: paths are quarantined as opaque strings (the U9 / M5
non-normalization contract still holds — see the new dedicated test).

Tests:
  - 8 isolated unit tests for the quarantine module — pins the
    interface contract (empty start, add/has/size, dedup on repeated
    add, no separator normalization, snapshot defensive copy + freshness,
    size-getter live behavior).
  - All 86 existing worker-pool tests pass unchanged — they exercise
    the quarantine through the pool and act as the regression net for
    behavior preservation.

Why not the full 5-module extraction in this commit: doc-review A10's
concern is real — a single-consumer abstraction adds module-boundary
overhead (5 sets of imports, 5 dedicated test files, 5 interfaces to
keep in sync with worker-pool) without any structural benefit until a
second consumer materializes. Extracting one validates the pattern;
the remaining four can be moved on demand.

* feat(workers): wire protocol.ts encoded IPC into parse-worker + pool (U17)

Production worker IPC now uses the U16 binary wire format (1-byte tag +
4-byte LE length + UTF-8 JSON body) end-to-end. The pool encodes every
outgoing `sub-batch` / `flush` dispatch via `encodeMessage`; the worker
decodes incoming frames via `decodeMessage` and encodes its `ready`,
`starting-file`, `progress`, `sub-batch-done`, `result`, `warning`, and
`error` outputs the same way.

The load-bearing correctness fix is making `decodeMessage` accept
`Uint8Array` rather than only `Buffer`: Node's `worker_threads`
`postMessage` structured-clones the payload, which strips the `Buffer`
prototype on the receive side. A frame sent as `Buffer` arrives as a
plain `Uint8Array`, and `Buffer.isBuffer(raw)` returns false — so the
first attempt at U17 (gating decode on `Buffer.isBuffer`) silently
treated every incoming frame as POJO and the worker never responded.
The fix adopts the underlying memory zero-copy via
`Buffer.from(view.buffer, view.byteOffset, view.byteLength)` and uses
`raw instanceof Uint8Array` at every call site (parse-worker decode,
pool dispatch handler, pool ready-handshake handler, FakeWorker test
mocks, and the integration-test worker preamble).

The pool stays tolerant of POJO incoming so unit-test FakeWorkers
don't need rewriting — only the new outgoing encoded dispatches require
the test scaffolding to decode on receive, which the test FakeWorkers
and the integration test's inline `parentPort.on` wrapper now do.

The slot-drop integration test was rewritten from a shared-counter-file
race (which pre-U17 timing happened to land on the assertion-friendly
counter==2 endpoint, but post-U17 protocol decoding latency shifted to
counter==1 and produced 3 quarantines instead of 2) to a deterministic
path-based crash trigger: slot 0 crashes on a.ts, respawns, crashes on
the requeued b.ts, slot is dropped after budget exhausted; slot 1
handles [c.ts, d.ts] normally. Outcome no longer depends on inter-worker
file-write ordering.

Protocol coverage adds two regression tests pinning the Uint8Array
decode path: structured-clone-stripped frames decode identically to
their Buffer originals, and Uint8Array views with non-zero byteOffset
into a wider ArrayBuffer also decode correctly (catches `Buffer.from(uint8)`
copying semantics if a future refactor loses the zero-copy adoption).

All 94 worker-pool tests (9 files, unit + integration) pass; the full
unit suite (6128 tests across 268 files) passes unchanged.

* perf(workers): zero-copy file content transfer via transferList (U19)

Pool dispatch now hoists `{path, content: string}[]` file contents OUT
of the U17 JSON envelope into separately-allocated `Uint8Array`s whose
ArrayBuffers are passed to `worker.postMessage`'s `transferList` for
zero-copy ownership transfer. The envelope itself carries only
lightweight metadata (`{path, byteLength}` per file) and is structure-
cloned the same as before.

What this saves vs U17 baseline:

- **JSON.stringify of file contents on main thread** drops to zero —
  the envelope is now O(paths + sizes), not O(total bytes). For a 200-
  file sub-batch of 10 KB TS files, that's ~2 MB of escape processing
  per dispatch that disappears. JSON.stringify's per-character branch
  on quotes/backslashes/control chars is roughly 2x slower than
  UTF-8 transcode in TextEncoder, so the replacement is a CPU win
  even though it adds a single TextEncoder.encode per file.
- **Structured-clone memcpy of file contents** drops to zero — the
  contents' backing ArrayBuffers are ownership-transferred, not copied
  into the worker's heap. The envelope's struct-clone cost is now
  proportional to metadata size only.
- **JSON.parse on worker thread** likewise no longer scales with
  content size. Worker decodes each `Uint8Array` to string via
  `TextDecoder` lazily at the parse boundary — runs on the worker
  thread, parallel with continued main-thread work, vs U17's
  sequential JSON.parse blocking the worker before processBatch can
  start.

Pipelining: TextEncoder.encode (main) and TextDecoder.decode (worker)
can both run while the OTHER side is doing useful work. Under U17,
struct-clone was a synchronous main-thread blocker.

The ArrayBuffer ownership contract is load-bearing:

- File-content `Uint8Array`s are allocated via `TextEncoder.encode`,
  NOT `Buffer.from(str, 'utf8')`. TextEncoder produces a dedicated
  ArrayBuffer per call; `Buffer.from(str)` carves from Node's shared
  `Buffer.poolSize` slab for small strings, so transferring one
  pool-backed Buffer's ArrayBuffer would detach every other Buffer
  that shares that slab — silent data corruption.
- The envelope itself is NOT transferred. It MAY be pool-backed by
  `encodeMessage`, and at ~30-80 bytes/file the struct-clone cost is
  negligible. Not transferring avoids the same detach-collateral risk
  the contents path is careful to dodge.

Detection is strict: every input element must have both `path: string`
and `content: string`. A single non-conforming element disqualifies
the whole batch from the transfer path and falls back to the legacy
single-Uint8Array `encodeMessage` envelope. Safer than partial
transfer (which would split a sub-batch into mixed-shape messages
the worker can't reassemble).

`parse-worker.ts` `decodeIncomingMessage` recognizes the hybrid
`{envelope, contents}` shape, decodes the envelope, zips metadata
positionally with the contents array, decodes UTF-8 → string per file,
and hands the reassembled `ParseWorkerInput[]` to the existing
`processBatch`. Identical downstream behavior to U17 — the IPC
optimization is invisible above this line.

Test scaffolding (3 FakeWorkers + 1 integration-test preamble) gain a
`decodeDispatchedMessage` helper that tolerates BOTH shapes (legacy
single-frame Uint8Array AND the new hybrid envelope+contents) so the
in-process unit mocks keep their existing action-scripting API and the
9 ad-hoc integration test workers keep their `msg.type === 'sub-batch'`
handlers unchanged.

`buildDispatchMessage` is now exported from worker-pool.ts so its
contract can be tested in isolation. A new
`test/unit/worker-pool-transferlist.test.ts` pins:
  - hybrid shape produced for parse-worker inputs
  - transferList carries one ArrayBuffer per file in input order
  - envelope decodes to metadata only (no `content` field)
  - content bytes round-trip byte-for-byte through UTF-8 (ASCII,
    multi-byte, surrogate-pair emoji)
  - each content's ArrayBuffer is independently allocated (no pool
    sharing) — the load-bearing transfer-safety invariant
  - non-parse shapes, empty arrays, and mixed-conformance arrays all
    fall back to the legacy single-frame path

All 271 test files (6166 unit + integration tests) pass.

* fix(workers,tests,docs): apply ce-code-review findings (16 items)

Walks the full set of findings from a multi-agent code review (11
reviewers, 1 maintainability dispatch lost to tool-permission denial)
of the PR #1693 branch. All 16 actionable findings — 4 P1, 4 P2,
8 P3 — applied in a single pass against a consistent tree. Tests
pass (269/269 unit files, 29/29 integration).

P1 — bounds-only / disguised-bounds assertions across 4 test files
(per user-memory DoD §2.7):
  - worker-pool.test.ts: 5 sites — `nodes.length > 0` dropped (redundant
    after `.toContain('validateInput')`); `files.length >= 4` pinned to
    `.toBe(7)` (mini-repo/src has exactly 7 .ts files); `results.length
    > 0` pinned to `.toHaveLength(1)` (default sub-batch absorbs all 7);
    `result.fileCount >= 0` pinned to `.toBe(1)` (empty file is still
    "processed"); `warnRecords.length > 0` replaced with content-
    predicate `/respawn|dropping|replacement|did not report ready/`
    (catches silenced warnings); `fallbackExcludePaths.length > 0`
    pinned to exact `['one.ts', 'two.ts']` (deterministic given the
    single-slot pool + 2 items + per-item starting-file).
  - parse-impl-fallback.test.ts: 3 sites — `astCacheClearCalls >= 1`
    pinned to exact 4 (per-chunk × 2 + finally × 2); the two error-path
    delta checks pinned to exact +2 and +3 (verified empirically).
  - parse-impl-progress-monotonic.test.ts: `percents.length > 0` →
    `.not.toEqual([])`; per-element `Math.max(prev, cur)` tautology
    replaced with direct `if (cur < prev) throw`; final-percent
    `Math.min(last, 95)` tautology pinned to exact `.toBe(70)` (3-file
    skipWorkers fixture's deferred band lands at the band start).
  - parse-impl-large-fixture.test.ts: `Math.min(elapsedMs, BUDGET)`
    tautology removed; Promise.race rejection is the load-bearing
    wall-clock check.

P1 — terminate() lacks `.catch` mask:
  - worker-pool.ts terminate() now matches the `.catch(() => undefined)`
    pattern used at every other internal terminate site. Prevents a
    hung/OOM worker's terminate rejection from masking the original
    pipeline error when called from parse-impl.ts's finally block, and
    guarantees `workers.length = 0` / `activeSlots.clear()` always run.

P1 — hybrid envelope length-mismatch + null-payload silent data loss:
  - parse-worker.ts decodeIncomingMessage: explicit non-null-and-typed
    check before `.type` access (decodeMessage permits null payloads
    per encodeMessage contract); explicit length-equality assertion
    between `decoded.files` and `contents` before zipping. Without
    these, `TextDecoder.decode(undefined)` silently returns "" and
    produces empty-content graph nodes — a contract violation that
    used to be undetectable. Both throws route through the outer
    try/catch → worker `error` reply → pool's recoverAndResume.

P1 — unsafe casts at the IPC boundary:
  - buildDispatchMessage now uses a properly-typed `isParseWorkerItemArray`
    type guard. The narrowed branch accesses `item.path` and
    `item.content` as statically-typed strings — a future rename of
    `ParseWorkerInput.content` would fail to compile inside the branch
    instead of silently mismatching at runtime. The remaining
    decodeMessage payload casts are bounded by the F3/F6 runtime
    guards.

P2 — idle-timeout retry bypasses circuit breaker:
  - worker-pool.ts timeout-retry IIFE now increments
    `consecutiveFailuresPerSlot[workerIndex]` alongside `respawnCount`.
    A slot that consistently times out (vs crashes) now trips the
    per-slot breaker, instead of consuming its full respawn budget
    over potentially tens of minutes without the breaker firing.

P2 — null/non-object worker message crashes pool handler:
  - Dispatch handler in worker-pool.ts now guards `null /
    non-object / no string type discriminant` before `msg.type` access
    and routes through recoverAndResume on violation. Previously a
    legitimate `null` payload would throw TypeError out of the
    EventEmitter listener → uncaughtException on main, crashing the
    analyze.

P2 — workerPoolSize === 0 creates unusable pool:
  - parse-impl.ts now treats `workerPoolSize === 0` as `skipWorkers`
    at the gate. Matches the PipelineOptions docstring contract ("0
    disables the pool entirely — equivalent to skipWorkers"); avoids
    constructing a pool that rejects every dispatch and logs
    "Worker pool parsing stopped" per chunk.

P2 — encodeMessage 2-buffer allocation per frame:
  - protocol.ts encodeMessage coalesced to a single
    `Buffer.allocUnsafe + writeUInt8 + writeUInt32LE + buf.write
    (string, offset, 'utf8')`. Drops the intermediate
    `Buffer.from(JSON.stringify(...), 'utf8')` allocation + memcpy.
    Length pre-check via `Buffer.byteLength(string, 'utf8')` surfaces
    the uint32 cap before any allocation.

P3 — slotGenerations made optional on WorkerPoolStats so external
  implementations of getStats() that predate U12 don't compile-break;
  in-repo callers already use optional chaining.

P3 — buildDispatchMessage marked `@internal` so it isn't surfaced as
  public API by typedoc / api-extractor (it's a test-only export).

P3 — verboseThroughputLog hoisted above the chunk loop (env vars can't
  change mid-run; one O(env-read) per analyze, not per chunk).

P3 — corrected the messageerror routing comment in worker-pool.ts
  dispatch handler. `ProtocolDecodeError` is caught by the surrounding
  try/catch — distinct from `messageerror`, which fires for V8
  structured-clone failures before the message body would reach the
  handler.

P3 — initial pool spawn now uses a `Promise.allSettled` ready-handshake
  gate symmetric with `replaceWorker`. Dispatch awaits this gate before
  selecting slots, so an init-crashing initial worker is dropped from
  `activeSlots` and a downstream OOM/missing-native-binding failure
  surfaces in seconds (bounded by WORKER_READY_TIMEOUT_MS) rather than
  waiting for the first idle timeout (30s default).

P3 — `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT`,
  `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS`,
  `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` added to:
    - CLI `--help` text in src/cli/index.ts
    - Root README env-var table
    - gitnexus/README troubleshooting section (new "Worker pool
      resilience tuning" subsection)

P3 — CLI `catch (e: any)` / `catch (err: any)` in analyze.ts replaced
  with `catch (err: unknown)` + narrowed access; matches modern TS
  best practice and the codebase pattern at other catch sites.

P3 — `WorkerPoolStats.terminated: boolean` field added (optional, for
  backward compatibility). `terminate()` sets it true; `getStats()`
  surfaces it. Distinguishes graceful shutdown from a circuit-breaker
  trip in observability surfaces.

Coverage / advisory items not addressed in this commit (kept in the
report only):
  - maintainability reviewer failed (Read/Bash denied) — god-module
    audit on worker-pool.ts (~1400 LOC) carried as residual risk
  - quarantine case-sensitivity contract unpinned (adversarial #8)
  - WORKER_READY_TIMEOUT_MS env-configurability (adversarial #2)
  - chunk-byte-budget × parseChunkConcurrency memory multiplier doc
    (adversarial #5)
  - MCP discoverability gaps for env vars / verbose (agent-native W1/W2)
  - bench/parse-throughput.md scaffold-with-TBD-rows (PS RR-003)

* fix(parsing): sequential gap-fill for worker-quarantined chunk files (U20.U1)

When the worker pool's Layer 3 quarantine filters one or more files
out of a chunk's dispatch, the worker results returned to
processParsing are silently narrower than the input chunk. Without
this reparse, the graph for this run would be missing every quarantined
file's symbols/imports/calls/heritage with no failure signal.

After the existing per-chunk quarantine log emits in
processParsing's worker-path try-block, run processParsingSequential
on JUST the quarantined-in-chunk files. The sequential path writes
directly to the graph, so symbols for those files land alongside
worker output for the surviving files.

Mirrors the WorkerPoolDispatchError catch-block's processParsingSequential
call shape — same signature, same args, same scopeTreeCache wiring.
Emits a structured warn naming `reparsedPaths` so operators can
observe the sequential fall-through.

This fixes the in-run side of the corruption Codex's adversarial
review of PR #1693 flagged. The cross-run side (chunk-cache
poisoning) is closed by U20.U2 in a follow-up commit.

References plan: docs/plans/2026-05-20-002-fix-chunk-cache-corruption-on-worker-quarantine-plan.md

* fix(parse-impl): suppress chunk-cache write when any chunk file was quarantined (U20.U2)

The chunk hash at parse-impl.ts:424-428 is computed from every file
in the chunk. The worker pool's Layer 3 quarantine
(worker-pool.ts createQuarantine) filters quarantined files out of
dispatch, so `rawResults` reflects only the surviving files. Before
this commit, the write at line 500-507 stored that partial result
under the full-coverage chunk hash — and on the next analyze with
unchanged content, the cache HIT branch (line 439-464) silently
replayed the incomplete result. Symbols from the quarantined file
were missing from the graph for as long as the cache survived.

Codex's adversarial review of PR #1693 flagged this as a silent-
corruption class because there's no failure signal: no warn log
during the replay, no graph-equivalence check, no exit code change.
The corruption only surfaces if an operator notices a missing symbol
in `gitnexus_query` output.

Guard the write with `chunkFiles.some(f => quarantineSet.has(f.path))`.
When any chunk file is in the worker pool's cumulative quarantine
snapshot, skip the `parseCache.entries.set` call. Emits a verbose-
only info log so operators investigating "why aren't my chunks
caching" have a diagnostic trail.

Skipping the write means the next analyze gets a cache miss for this
chunk and re-dispatches it. Quarantine is session-scoped (a fresh
createWorkerPool starts with an empty quarantine), so the new pool
gives the quarantined file another chance. If quarantine fires again,
U20.U1's sequential gap-fill still produces a complete graph for that
run; the cache stays empty for the chunk until a fully-clean
dispatch lands.

The cache-hit replay branch at parse-impl.ts:439-464 is unchanged.
Its contract strengthens: "cache entries are complete" becomes true
post-fix, but the replay code doesn't need to know that.

Closes the cross-run side of the Codex finding. U20.U3 adds the
regression test.

References plan: docs/plans/2026-05-20-002-fix-chunk-cache-corruption-on-worker-quarantine-plan.md

* test(parse-impl): integration regression for quarantine + chunk-cache (U20.U3)

Pins the U20 fix end-to-end via REAL `worker_threads` + `createWorkerPool`.
Mirrors the writeReadyWorker pattern from `test/integration/worker-pool.test.ts`
— inline READY_PREAMBLE + custom test worker script that:

  1. Decodes the U17/U19 IPC protocol (Buffer frame OR hybrid envelope/
     contents shape) the same way the production parse-worker does.
  2. Emits a `{type:'ready'}` handshake so the pool's
     `waitForWorkerReady` resolves promptly.
  3. On a sub-batch containing `poison.ts`, emits starting-file +
     `process.exit(134)`. The pool attributes the death to `poison.ts`
     via the in-flight signal and adds it to the session-scoped
     quarantine.
  4. On a sub-batch without poison, synthesizes a minimal valid
     `ParseWorkerResult` with one `Function` node per file (no
     tree-sitter dep in the test worker — the synthesized nodes give
     `mergeChunkResults` deterministic content for the graph).

Assertions exercise both fix layers:

  - U1 (sequential gap-fill in processParsing): the graph contains a
    `Function` node named `poison` AFTER the run. The custom worker
    never emits anything for `poison.ts`, so the only path for that
    symbol to reach the graph is `processParsing`'s sequential
    reparse of the quarantined-in-chunk file using the real
    tree-sitter parser against the actual source.
  - U2 (cache-write suppression in runChunkedParseAndResolve):
    `parseCache.entries` does NOT contain the chunk hash after the
    run; `parseCache.usedKeys` DOES contain it (chunk processed,
    cache write specifically skipped).
  - Cross-run: a second pass over the same fixture with the same
    parseCache and a fresh worker pool re-dispatches the chunk
    (cache empty), the worker crashes again, sequential gap-fill
    runs again, and the cache stays empty. Pins the round-trip
    contract.

Adds `workerUrlForTest?: URL` to PipelineOptions — same `@internal`
test-only injection precedent as `workerThresholdsForTest` (already
in PipelineOptions for thresholds). When set, parse-impl uses the
provided URL instead of the src/ → dist/ resolution dance. Production
call sites never set this field; the only consumer today is this
integration test.

Why integration over unit:
  - The fix lives at the boundary between parsing-processor.ts and
    parse-impl.ts under a real WorkerPool. Unit-mocking the
    worker-pool module bypasses the structured-clone boundary, the
    dispatch lifecycle, and the actual quarantine flow — it verifies
    the test setup rather than the contract. The real worker thread
    executing through the U17/U19 IPC protocol IS the load-bearing
    surface.
  - User-explicit preference (saved as
    feedback_integration_over_vimock.md memory). For worker-pool /
    parse-impl / IPC-touching code: write integration tests under
    test/integration/ using writeReadyWorker patterns; avoid
    vi.mock on worker-pool.js.

Test wall-clock: under 2s; both `it` blocks together complete in
~1.8s under the existing CI conditions.

References plan: docs/plans/2026-05-20-002-fix-chunk-cache-corruption-on-worker-quarantine-plan.md

* refactor(parsing): remove sequential-parser fallback (U20 design pivot)

The worker pool's resilience layers — respawn budget, circuit breaker,
quarantine, slot-attribution, cumulative timeout — are now the SOLE
contract for handling worker failures. Two sequential-reparse paths
are removed from processParsing:

1. **U20.U1 sequential gap-fill for quarantined chunk files** (just
   added in commit 7dd489e9, now reverted). The pre-emptive rescue
   would re-run processParsingSequential on the file that ALREADY
   killed a worker — which for the most common quarantine cause
   (tree-sitter native SIGSEGV on a pathological file) re-triggers
   the same native crash on the main thread, killing the entire
   analyze. The "rescue" turned silent missing-symbols into a louder
   analyze-wide crash. Drop the rescue; accept the per-run gap.

2. **Pre-existing WorkerPoolDispatchError catch-block sequential
   fallback** (in production since PR #1693's resilience layer
   landed). Same risk class — when the pool exhausts its respawn
   budget / trips the circuit breaker, the failing files are
   precisely the ones likely to crash a sequential parser too. The
   "graceful degradation" hid pool failures behind degraded-but-
   completing analyze runs, making operational issues harder to
   surface and diagnose. Drop the catch-block; WorkerPoolDispatchError
   propagates to the analyze entry point where the user sees a clear
   hard signal.

What stays:
- The `skipWorkers: true` / small-repo path that uses
  `processParsingSequential` as the EXPLICIT primary path (not a
  fallback). Caller-driven opt-out and tiny-repo perf optimization
  are different intents.
- U2's chunk-cache write suppression in parse-impl.ts (commit
  7c9c9556). When quarantine fires, the chunk stays uncached so the
  next analyze with a fresh pool retries the file cleanly. That's
  the cross-run correctness Codex's adversarial review actually
  asked for.
- The per-chunk quarantine warn log (parsing-processor.ts) — operators
  see which files were skipped, both immediately and across runs.

What changed:
- `processParsing` worker-path try-block: unwrapped. The
  `processParsingWithWorkers` call is now direct (no try/catch
  wrapping); errors propagate to the chunk-loop caller.
- `parsing-worker-fallback.test.ts` rewritten: the previous 5 tests
  asserted graceful sequential-fallback behavior. Replaced with 3
  tests pinning the new contract — raw Error propagates, WorkerPool-
  DispatchError propagates with fallbackExcludePaths intact, normal
  quarantine signal does NOT throw and surfaces via progress detail.
- `parse-impl-quarantine-cache-skip.test.ts` (U20 integration test)
  updated: poison.ts is NOT in the post-run graph; surviving files
  are; chunk-cache stays empty; second pass re-dispatches and leaves
  cache empty.
- Plan doc updated to mark R1 as dropped and explain the U20 pivot
  in the Summary.

User decision: explicit directive ("let's remove the sequential
fallback entirely we must rely on entirely that the parallel process
is resilient enough to work itself through the code base"). The pool's
resilience layers are designed for this — respawn budget, circuit
breaker, quarantine, slot-generation, cumulative-timeout cap — and
adding a layer below them was redundant insurance with real downside.

Tests: 269/269 unit files (6135 tests) green. 31/31 worker-pool +
parse-impl integration tests green. The 2 reported "errors" in the
integration run are the pre-existing intentional-process.exit unhandled-
exception leaks from test workers — unchanged by U20.

References plan: docs/plans/2026-05-20-002-fix-chunk-cache-corruption-on-worker-quarantine-plan.md

* fix(workers,tests,docs): address ce-ultrareview findings F1/F2/F3/F4

Multi-lane review run on the PR #1693 branch surfaced four addressable
items beyond the blocking three.

F1 (minor, CodeQL): unused `findMatch` helper in
test/unit/scope-resolution/typescript/typescript-captures-anchor.test.ts:28
removed. `countMatchesTsx` flagged by the same CodeQL pass is a false
positive — it's called at line 88 by the JSX-anchor regression tests
so the rewrite case actually fires under TSX, not just TS.

F2 (medium, docs): bench/parse-throughput.md retitled as
"(scaffold)" with an explicit "no measurement data has been collected
yet" note above the table. The self-contradictory "Regenerate this
file before merging any PR that touches the ingestion pipeline"
instruction is dropped — the file ships intentionally without
numbers; the load-bearing perf-regression protection lives in
test/integration/parse-impl-large-fixture.test.ts (U6, 30s
Promise.race wall-clock budget). The Latest measurement section now
preserves the ~6s sequential observation as a smoke reference, not as
a regression target.

F3 (low, API hygiene): `WorkerPoolDispatchError.fallbackExcludePaths`
renamed to `quarantinedPaths`. The "fallback" terminology was
load-bearing under the pre-U20 design when `processParsing`'s
sequential-fallback catch-block consumed it to filter the fallback
file list. After commit be1f65c removed that catch-block, no
production code reads the field — but it stays populated by the pool
because the snapshot is genuinely useful operator diagnostics when
the breaker trips. The rename clarifies the field's actual semantics
(here are the files the pool quarantined before it tripped) without
changing wire behavior. Definition + the lone surviving in-pool
comment reference + both test assertions updated.

F4 (low → real fix, reliability): timeout-retry IIFE in
worker-pool.ts now consults `consecutiveFailureThreshold` and trips
the circuit breaker when the per-slot consecutive-failure count
crosses it. Closes a gap left by ce-code-review's REL-02 patch — that
fix added the `consecutiveFailuresPerSlot[workerIndex]++` increment
in the timeout-retry path but did NOT add the corresponding
threshold-check + tripBreaker call. Result: chronic pure-timeout
deaths accumulated counts that never tripped the breaker until the
slot also hit `respawnCount > maxRespawnsPerSlot`. Now timeouts and
crashes are structurally treated the same way by the breaker, which
is what the REL-02 increment was meant to enable. Test coverage:
worker-pool-resilience.test.ts already exercises the breaker via the
shared handleWorkerDeath path; this new branch traces the same
trip semantics with a different entry point, so the breaker-tripped
state is observable via the same `getStats().poolBroken` and
`WorkerPoolDispatchError.quarantinedPaths` surface.

Out of scope here (caller actions or future PRs):
  - F5 (info): cumulative-quarantine cache check is safe in practice
    because chunks are alphabetically deterministic; no action.
  - F6 (low): exit-code-0 quarantine exemption — pre-existing P2
    residual, bounded by quarantine + respawn budget; deferred.
  - F7 (info): dispatch non-reentrancy contract documented but not
    enforced; no production caller violates it; deferred.
  - PR title `[WIP]` removal — happens on GitHub side.

Tests: 274/274 test files (6185 passing, 30 skipped). The single
"error" in the integration runner is the pre-existing intentional-
process.exit unhandled-exception leak from the deliberate startup-
crash test worker, unchanged by these fixes.

* fix(workers): swap protocol body from JSON to V8 serialize/deserialize

CI scope-parity tests on Ubuntu surfaced silent data loss in the
worker IPC: `Phase 'scopeResolution' failed: scope.typeBindings is not
iterable` (Python, Go) and `importerModule.typeBindings.has is not a
function` (Python). Plus three #1066 large-file regression tests
(Python / C# / TypeScript) failed because call relationships weren't
resolving from the worker output.

**Root cause:** U17 introduced `JSON.stringify`/`JSON.parse` as the
protocol body codec. JSON has no representation for `Map`, `Set`,
`Date`, `RegExp`, `BigInt`, `TypedArray`, `undefined` values, or
circular refs — `JSON.stringify(someMap)` returns `"{}"`. Production
scope-resolution code keys data structures on Maps throughout
(`ParsedFile.scopes[*].typeBindings: ReadonlyMap<string, TypeRef>`,
plus `bindings`, `bySourceScope`, `byTargetDef`, the finalize-algorithm
edge indexes, etc.). The JSON round-trip silently turned every Map
into an empty object, manifesting downstream as iteration / `.has`
calls failing on the decoded payload.

**Fix:** replace the JSON body with `node:v8`'s `serialize` /
`deserialize`. That's the same structured-clone algorithm Node's
`worker.postMessage` uses natively — bit-for-bit compatible with the
pre-U17 implicit-clone path. Full type fidelity for Map, Set, Date,
RegExp, BigInt, TypedArray, undefined values, and circular refs. No
external dependency.

A previous iteration of this fix attempted to bolt a Map/Set
replacer+reviver onto the JSON path. Rejected in favor of V8
serialization because:
  - the JSON tag-marker approach requires per-type registration
    (Map, Set; then Date, RegExp, BigInt would each need their own
    sentinels); V8 handles them all uniformly
  - keys to JSON-encode would still need handling for nested types
    (and the marker approach doesn't survive nested Maps-in-Maps
    cleanly without recursive replacer logic)
  - V8 is faster than JSON for object-heavy payloads anyway (binary
    format, no string escaping pass)
  - the user-explicit ask was "a much more generic solution that will
    work for everything" — V8 serialization IS the generic solution

Trade-offs documented in the module header:
  - body bytes are opaque (binary, not human-readable) — debugging
    requires `v8.deserialize` ad-hoc; protocol.test.ts exercises every
    supported MessageTag including the new type-fidelity cases as a
    regression net.
  - format is tied to the running Node major. Pool always spawns
    workers on the same Node instance the main thread runs, so this is
    moot in production. Would matter if frames ever persisted to disk
    (nothing does today).

Protocol test file rewritten:
  - drops the JSON-specific byte-layout assertions (e.g. `body must
    equal "null" string`) — replaced with V8-derived expected lengths
  - adds a "structured-clone type fidelity" describe block that pins
    Map, nested Map, Set, Date, RegExp, BigInt, TypedArray, undefined
    values, and circular-ref round-trips. These are the load-bearing
    regression tests preventing a future "optimize" PR from quietly
    swapping V8 back to JSON.
  - the bad-body decode-error test now uses arbitrary non-V8 bytes
    instead of `{not-json}` — same intent.

Integration test READY_PREAMBLEs (worker-pool.test.ts and
parse-impl-quarantine-cache-skip.test.ts) update their inline
decoders to use `v8.deserialize` matching the production codec.
Both files have a standalone CJS worker preamble that can't import
dist/protocol.js by relative path, so the V8 dependency is required
via `node:v8` directly.

Tests: 271/271 unit files (6163 tests + 30 skipped). 28/28
worker-pool integration. 3/3 parse-impl integration. 791/791
scope-parity tests (the four CI-failing files: python.test.ts,
go.test.ts, typescript.test.ts, csharp.test.ts) all green again.

References plan: docs/plans/2026-05-20-002-fix-chunk-cache-corruption-on-worker-quarantine-plan.md

* refactor(workers): drop protocol.ts; use native postMessage + transferList

The protocol.ts framing layer was redundant — Node's `worker.postMessage`
already runs V8 structured-clone internally, the same algorithm that
backed `v8.serialize`. Wrapping V8.serialize → Buffer →
postMessage(struct-clone-Buffer) was a double-walk: one full
structured-clone pass to produce the Buffer, then another pass when
postMessage cloned that Buffer across threads. This commit cuts the
wrapper layer; workers and pool exchange POJO directly via
`worker.postMessage(value, transferList)`, with file-content
`ArrayBuffer`s in `transferList` for zero-copy ownership transfer.

What changes:

- **Deleted** `src/core/ingestion/workers/protocol.ts` (~180 LOC) +
  `test/unit/workers/protocol.test.ts` (~250 LOC). The MessageTag
  enum / ProtocolDecodeError / encodeMessage / decodeMessage surface
  is gone. Tag-based routing is replaced by the `msg.type`
  discriminant that every receive site already checks. Protocol-decode
  errors map to Node's `messageerror` event (V8 deserialization
  failures during postMessage), which the pool already wires to
  `recoverAndResume`.
- **`worker-pool.ts`**: `decodeIncomingWorkerMessage` removed; handlers
  receive POJO directly. `buildDispatchMessage` now returns
  `{message: {type:'sub-batch', files: [{path, content: Uint8Array}]},
  transferList: ArrayBuffer[]}`. The Uint8Array-per-content allocation
  via `TextEncoder.encode` is preserved (it's the load-bearing
  transfer-safety contract that keeps content out of Node's shared
  `Buffer.poolSize` slab). Flush dispatch is now plain
  `worker.postMessage({type:'flush'})`.
- **`parse-worker.ts`**: `decodeIncomingMessage` removed. The message
  handler receives POJO directly; the only conversion is
  `Uint8Array → string` for sub-batch file contents at the
  `decodeSubBatchFiles` boundary, before handing to `processBatch`.
  Outgoing messages are emitted as POJO via plain
  `parentPort.postMessage({type:'starting-file', ...})` etc. The
  `sharedHybridDecoder` is now `sharedContentDecoder` (same intent,
  clearer name for the simpler shape).
- **Test scaffolding**: FakeWorkers in `worker-pool-resilience`,
  `worker-pool-windows-quarantine`, and `worker-pool-slot-generation`
  drop their `decodeMessage` import + `decodeDispatchedMessage` helper.
  The helpers stay (still convert `files[i].content` Uint8Array →
  string for test-action introspection) but no longer touch any
  protocol framing — just shape-check for sub-batch.
- **Integration READY_PREAMBLEs** (worker-pool.test.ts and
  parse-impl-quarantine-cache-skip.test.ts): drop the inline
  v8.deserialize + envelope-unzip logic; the preamble is now just
  the ready handshake + a `parentPort.on` wrapper that converts
  `files[i].content` Uint8Array → string for the ad-hoc test worker
  scripts.
- **`worker-pool-transferlist.test.ts`**: contract tests updated for
  the new buildDispatchMessage shape — no `envelope` field anymore;
  `message.files[i].content` is Uint8Array; transferList holds each
  content.buffer in input order. Pool-slab independence still pinned.

What stays the same:

- Zero-copy file-content transfer via transferList — every file's
  ArrayBuffer is ownership-transferred to the worker (no copy).
- Full structured-clone type fidelity — Map / Set / Date / RegExp /
  BigInt / TypedArray / undefined / circular refs all preserved by
  Node's native postMessage. The V8 fix from commit 06f6957e is
  inherent in this path; there's no JSON layer to lose them.
- TextEncoder-per-content allocation — keeps content buffers out of
  the shared `Buffer.poolSize` slab so transferring one cannot detach
  another.
- The pool's resilience layers (respawn, breaker, quarantine,
  starting-file attribution, cumulative timeout, ready handshake,
  slot-generation guard) — unchanged.
- U20 chunk-cache write suppression on quarantine — unchanged.

Net: ~430 LOC removed (protocol.ts + tests + inline decoders + helpers),
~120 LOC simplified in worker-pool.ts and parse-worker.ts. One less
serialization pass per message on the hot path.

Tests: 270/270 unit files (6133 + 30 skipped). 822/822 integration
tests including the four CI-failing scope-parity files (Python, Go,
TypeScript, C#) — the V8-fidelity contract holds via native
postMessage with no explicit serializer. The single "error" reported
in worker-pool.test.ts is the pre-existing intentional
process.exit unhandled-exception artifact from the deliberate
startup-crash test, unchanged by this commit.

* refactor(parse-worker): drop legacy single-message dispatch mode

The `parentPort.on('message', ...)` handler had an `Array.isArray(msg)`
branch left over from a pre-sub-batch dispatch shape — the pool used
to send the items array directly, before the worker pool added
sub-batching and the `{type:'sub-batch', files: ...}` envelope.

No production caller has dispatched that shape since the sub-batching
refactor landed; verified by grepping the repo for `postMessage([`
patterns (zero matches). The `ParseWorkerInput[]` arm in the
`WorkerIncomingMessage` discriminated union also blocked
exhaustiveness narrowing — flagged by the kieran-typescript code
review (RR-01) as "if a future unit removes the legacy array path,
this arm should be dropped." Dropping it now.

What changes:
  - Remove the `Array.isArray(msg)` branch from the message handler.
  - Drop `ParseWorkerInput[]` from the `WorkerIncomingMessage` union;
    it's now a clean `{type:'sub-batch'} | {type:'flush'}` discriminated
    union, so the dispatch switch is exhaustive over `msg.type`.

Tests: 71/71 worker-pool unit + integration tests green (resilience,
slot-generation, windows-quarantine, transferlist, parsing-worker-
fallback, worker-pool integration, parse-impl-quarantine-cache-skip).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-20 20:39:35 +01:00
Copilot
f350ae278a
feat: Add analyze --repair-fts, enforce FTS verification, and harden repair safeguards (#1720)
* Initial plan

* feat(analyze): add --repair-fts and verify FTS index rebuilds

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/dccb3673-af86-43aa-aede-2e1449399775

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor(fts): tighten repair/verify messaging and option naming

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/dccb3673-af86-43aa-aede-2e1449399775

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docs: highlight analyze --repair-fts vs --force in READMEs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/61edc967-debc-419f-9f51-aebf2ef08d22

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(analyze): guard repair mode against missing graph store

* fix(cli): reject --repair-fts with --force

* test(analyze): document repair-store fixture intent

* test(analyze): tidy repair failure fixtures and constants

* test(analyze): clarify mock constants in repair tests

* test(analyze): rename simulated missing-index constant

* test(analyze): clarify mocked graph shape in full-verify test

* refactor(analyze): finalize flag validation and test clarity

* test(skip-git): avoid hard failing when FTS extension is unavailable

* test(skip-git): log visible FTS-unavailable test skips

* test(skip-git): tighten FTS-unavailable error detection

* test(skip-git): simplify FTS-unavailable message checks

* test(skip-git): avoid HOME pointing at parent repo in fixture env

* fix(analyze): address Claude follow-up findings for repair guardrails

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2f7243d3-ba16-4d83-86e5-17e6c58a3b0d

* fix(repair-fts): clarify invalid graph-store preflight errors

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2f7243d3-ba16-4d83-86e5-17e6c58a3b0d

* test(analyze): strengthen assertions for conflict and missing-store errors

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2f7243d3-ba16-4d83-86e5-17e6c58a3b0d

* fix(repair-fts): make invalid graph-store type errors explicit

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2f7243d3-ba16-4d83-86e5-17e6c58a3b0d

* fix(repair-fts): improve graph-store type diagnostics

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/2f7243d3-ba16-4d83-86e5-17e6c58a3b0d

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-20 13:37:04 +01:00
Shane Thurston Wijaya
33f18ceaa2
feat(eval-server): added --host for user configured host IP instead of system hardcoded IP (127.0.0.1) (#1667)
* feat(eval-server): added --host for user configured host IP instead of system hardcoded IP (127.0.0.1)

* fix(eval-server): localhost value in --host now returns 127.0.0.1 instead of the raw input to fix wrong address, handled error for ipv6 disabled containers

* feat(eval-server): add --host flag with validation and error handling

  Co-Authored-By: Val Vladescu <val.vladescu@thirdbridge.com>

* fix(eval-server): bracketed IPv6 addresses to remove ambiguity

* docs(eval-server): document --host flag, READY signal format, and parser migration note

* fix(eval-server): use actual bound port in READY signal; strengthen --host e2e tests

  Co-Authored-By: Val Vladescu <val.vladescu@thirdbridge.com>

* feat(eval): wire eval-server --host through gitnexus_docker.py

* docs(eval): added guidance for docker user

* docs(eval): revise the imprecise documentation

* fix(e2e): updated original stdout for new format
2026-05-18 16:00:42 +01:00
Shane Thurston Wijaya
105efd0f7c
feat(wiki): added --lang <lang> flags to gitnexus wiki for multilanguage wiki generation support (#1613) 2026-05-17 19:54:02 +01:00
Copilot
ed50a6729f
fix(wiki): Remove the hidden 60s default timeout, validate gitnexus wiki timeout/retry flags, and surface timeout errors (#1651) 2026-05-17 12:03:54 +01:00
Shane Thurston Wijaya
88d3df77cc
feat:(wiki) added --timeout and --retries flags for large module pages to mitigate timeout aborts (#1543)
* feat:(wiki) added --timeout and --retries flags for large module pages to mitigate timeout aborts

* docs(wiki): document --timeout and --retries options

* docs(wiki): document --timeout and --retries in SKILL.md

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-13 18:35:39 +01:00
achianuri
fdf1effb2a
feat(cli): add --skip-skills and --index-only flags to analyze (resubmit of #742) (#1485)
* feat(cli): add --skip-skills and --index-only flags to analyze command

The `installSkills()` call in `generateAIContextFiles()` runs
unconditionally, injecting 6 skill files into `.claude/skills/gitnexus/`
even when `--skip-agents-md` is passed. This is problematic for bulk
indexing operations on read-only mirrors or third-party repos.

Add two new flags:
- `--skip-skills`: suppress standard GitNexus skill file injection
- `--index-only`: pure index mode that suppresses all file injection
  (AGENTS.md, CLAUDE.md, and skills), writing only to `.gitnexus/`

This gives users three levels of control:
- `--skip-agents-md` — suppress only root context files
- `--skip-skills` — suppress only skill injection
- `--index-only` — suppress everything (pure indexing)

Discovery context: while bulk-indexing 176 repos with
`--skip-agents-md`, all 144 indexed repos were contaminated with
`.claude/skills/gitnexus/` files requiring manual cleanup.

* fix(cli): address PR #742 review — gate community skills, drop dangling refs, add tests

Bot review (#742) flagged three issues with the original commit:

1. `--index-only --skills` still wrote community-derived skill files
   to `.claude/skills/generated/`. The `--skills` branch in analyze.ts
   was not gated by `skipAll`, so the "skip all file injection" contract
   was violated. Gate `generateSkillFiles()` with `!skipAll` so
   `--index-only` truly wins over `--skills`.

2. `--skip-skills` without `--skip-agents-md` produced AGENTS.md /
   CLAUDE.md that still referenced `.claude/skills/gitnexus/*/SKILL.md`
   files that were never installed — every agent load incurred 6
   failed reads. Pass `skipSkills` through to `generateGitNexusContent()`
   and omit the standard-skill rows (and the entire `## CLI` heading
   when the table is empty). Community skills, when present via
   `--skills`, are unaffected.

3. No filesystem tests for `skipSkills` / `indexOnly`. Add three
   regression guards to `test/unit/ai-context.test.ts`:
   - `.claude/skills/gitnexus/` is NOT created when skipSkills=true
   - Nothing is written when both skipAgentsMd and skipSkills are true
     (the resolved-flag state from --index-only)
   - AGENTS.md/CLAUDE.md routing table omits standard skill references
     when skipSkills=true, but preserves the load-bearing imperative
     sections (Always Do / Never Do / Resources)

* test(cli): PR 1485 review follow-ups (help text, gate test, --skip-skills docs)

- Assert --skip-skills and --index-only in analyze --help (skip-git-cli.test.ts).

- Export shouldGenerateCommunitySkillFiles; unit-test index-only+skills gate.

- Clarify --skip-skills does not suppress --skills community files; --index-only for full skip.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): warn when --index-only silently overrides --skills

Address review findings on PR 1485 follow-ups:
- analyze.ts emits a one-line note when both --index-only and --skills
  are set, so users see why a pipeline re-index ran with no skill files
  written.
- index.ts --skills help text now flags the --index-only override.
- shouldGenerateCommunitySkillFiles JSDoc documents the dual role of
  the gate (community skills + AGENTS.md/CLAUDE.md re-generation).
- skip-git-cli.test.ts pins the override-warning surface end-to-end.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 15:00:58 +01:00
Alex Macdonald-Smith
d91428ad9d
feat(cli): add gitnexus publish for opt-in understand-quickly registry (#1425)
* feat(cli): add `gitnexus publish` for opt-in understand-quickly registry

Adds a small, opt-in command that fires a single `repository_dispatch`
event at `looptech-ai/understand-quickly` to ask the registry for an
instant resync of the current repo's entry. No graph file is uploaded;
the registry pulls from raw.githubusercontent.com per the protocol at
https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md.

  - Pure helpers (id parsing, payload construction, validation) live in
    `gitnexus-shared/src/integrations/understand-quickly.ts` so the
    package stays Node-free and the same logic is testable in isolation.
  - The CLI command lives in `gitnexus/src/cli/publish.ts`. Without
    `UNDERSTAND_QUICKLY_TOKEN` it is a no-op (exits 0 with one
    informational line); with the token it POSTs the dispatch and
    surfaces 204 / 401 / 404 / 5xx distinctly.
  - The id defaults to `<owner>/<repo>` parsed from the `origin` remote
    and can be overridden with `--id`.
  - Refuses to publish when no `.gitnexus/` index exists, with a
    `gitnexus analyze` hint.

Tests: a new vitest unit covers the pure helpers (8 + 8 + 2 cases) and
the no-token no-op path with a `fetch` spy that fails the test if the
network is touched. README gets a one-paragraph "Publishing to
understand-quickly" section near the existing CLI docs.

* fix(uq-publish): address review blockers + high-severity items

Addresses CodeQL polynomial-regex (HIGH), token-gate ordering, distinct
401/403/404/422 response branches, fetch timeout, expanded test coverage,
tightened owner/repo validation, and non-GitHub remote rejection.

See response thread on PR #1425 for the per-finding rationale.

Signed-off-by: amacsmith <alex.mac@looptech.ai>

* fix(publish): address Claude review on PR #1425

- AbortError → TimeoutError: AbortSignal.timeout() throws a
  DOMException with name 'TimeoutError', not Error{name:'AbortError'}.
  Match the pattern used in core/embeddings/http-client.ts so the
  user-facing "timed out after 15000ms" message actually fires. Update
  the regression test to throw a real DOMException — the previous fake
  was a false-green.
- isValidOwnerRepo: forbid trailing hyphen in the owner segment.
  GitHub rejects this at account-creation time; allowing it here meant
  hand-typed --id values like 'my-org-/repo' would pass our regex and
  422 from GitHub.
- Add publish-command coverage to cli-index-help.test.ts (asserts on
  --id, --skip-git, the registry name, and the token env var) and
  cli-commands.test.ts (asserts publishCommand is exported as a
  function). Catches accidental command-registration deletion.

---------

Signed-off-by: amacsmith <alex.mac@looptech.ai>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-09 09:52:26 +01:00
Jorrin
96578aa4a8
feat: add optional limit arg to --embeddings flag (closes #382) (#1375) 2026-05-06 16:31:28 +01:00
Copilot
6372b0bfeb
fix(cli): --skip-git treats cwd as index root instead of walking up to parent git repo (#1245) 2026-05-01 09:49:48 +01:00
Gergő Magyar
2a0c97c178
fix: add platform-aware semantic fallback (#1150)
* fix: add platform-aware semantic fallback

Make VECTOR an optional capability so Windows analysis remains stable while semantic embeddings can fall back to exact scan when native vector indexing is unavailable.

Made-with: Cursor

* fix: remove stale vector pool import

Keep the merge with main lint-clean after VECTOR loading moved out of the read pool.
2026-04-28 12:21:25 +01:00
Gergő Magyar
38ccf7ceb1
fix: recover worker parse stalls (#1121)
* fix(ingestion): recover worker parse stalls

Made-with: Cursor

* test(ingestion): cover worker timeout controls

Made-with: Cursor

* docs: document analyze worker timeout controls

Made-with: Cursor

* fix(ingestion): fail fast after worker pool hard failure

Made-with: Cursor

* test(ingestion): stabilize worker stall recovery tests

Made-with: Cursor

---------

Co-authored-by: GitNexus Maintainer <maintainer@gitnexus.local>
2026-04-27 20:07:03 +01:00
Copilot
2b0392cd83
feat(analyze): preserve existing embeddings by default; --force regenerates them; add --drop-embeddings opt-out (CLI + HTTP API) (#1055)
* Initial plan

* fix(analyze): preserve existing embeddings by default; add --drop-embeddings opt-out

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/da1da041-afcd-4d38-8a2f-39ca52a462ff

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* analyze: --force on embedded repo now regenerates embeddings (preserve+top-up)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e2759765-b8f6-453a-8c28-595439d23cb4

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* analyze: wire dropEmbeddings into HTTP API; log cache-load failures; extract pure deriveEmbeddingMode + behavioral tests; sync GUARDRAILS.md

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7d88e595-cbd8-47b2-ba4f-fb5b9a60cda4

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-24 13:07:40 +01:00
azizur100389
ee871419e1
feat(ingestion): GITNEXUS_INDEX_TEST_DIRS opt-in for __tests__ / __mocks__ (#771) (#1046)
* feat(ingestion): GITNEXUS_INDEX_TEST_DIRS opt-in for __tests__ / __mocks__ (#771)

The DEFAULT_IGNORE_LIST hardcodes __tests__ and __mocks__ as
auto-filtered directory names. The comment at ignore-service.ts:273
explicitly documents this as intentional — .gitnexusignore negation
cannot override hardcoded entries. That default is right for the
majority of users, but for Quality Engineering workflows where test
files are the primary index target (tracing coverage via CALLS
edges), there was no escape hatch short of patching the installed
package.

Add an opt-in env var mirroring the GITNEXUS_NO_GITIGNORE /
GITNEXUS_MAX_FILE_SIZE precedent:
`GITNEXUS_INDEX_TEST_DIRS=1` removes __tests__ / __mocks__ from the
effective ignore set. Scope is deliberately limited to these two
names — the issue asked for these specifically, and other
test-adjacent entries (__snapshots__, snapshots, fixtures, .jest)
remain auto-filtered unchanged. .gitnexusignore negation semantics
are not touched; the env var is the orthogonal escape hatch.

Implementation: new `isEffectivelyIgnoredDirectory` helper in
ignore-service.ts wraps the `DEFAULT_IGNORE_LIST.has(name)` check
with the env-var opt-out. Two call-sites swap: shouldIgnorePath
(affects filesystem walker and wiki generator) and
createIgnoreFilter.childrenIgnored (affects directory pruning
during traversal). `isHardcodedIgnoredDirectory` export unchanged —
its contract is "is in the raw list", which remains true for
__tests__ / __mocks__ regardless of env var state (locked in by a
test).

Default behaviour is byte-identical for users who don't set the env
var. 9 new unit tests cover default-unset, opt-in-set, scoped scope
(other hardcoded entries unaffected), and the scope-discipline
guard (future expansion beyond the two named dirs fails loudly).
Env state restored by afterEach to prevent leakage.

Closes #771.

* feat(ingestion): .gitnexusignore negation overrides hardcoded DEFAULT_IGNORE_LIST (#771)

Per @magyargergo's review feedback: rather than add a special-case
GITNEXUS_INDEX_TEST_DIRS env var to unlock __tests__ / __mocks__,
let .gitnexusignore use !pattern negation to override the hardcoded
DEFAULT_IGNORE_LIST — mirroring the .gitignore mental model users
already know.

Implementation:
- New private hasExplicitUnignore(ig, rel) helper that walks ancestor
  segments and uses ignore.test(path)'s `unignored` flag to detect
  explicit negation. Ancestor-walk is required because .gitignore
  negation propagates — !__tests__/ implicitly unignores every
  descendant, but ignore.test() only reports unignored: true on the
  directly-matched path.
- createIgnoreFilter.ignored() and .childrenIgnored() now check
  hasExplicitUnignore BEFORE applying the hardcoded DEFAULT_IGNORE_LIST.
  If any ancestor (or the path itself) was explicitly unignored in
  .gitnexusignore, the hardcoded block is bypassed.
- shouldIgnorePath stays pure hardcoded-list — the wiki generator and
  other callers without per-repo config context keep deterministic
  behavior. The #771 override lives only inside createIgnoreFilter,
  which IS called with config.

Dropped:
- GITNEXUS_INDEX_TEST_DIRS env var (superseded by the more general
  negation mechanism)
- isEffectivelyIgnoredDirectory helper
- Associated env-var help text and unit tests

Added:
- Tip in analyze --help pointing users at .gitnexusignore with
  !__tests__/ as the example
- 8 new unit tests covering default behaviour, directory-level
  negation, selective overrides, generalisation (!node_modules/),
  non-leakage across hardcoded entries, standard non-negation rules
  still layering on top, and preservation of shouldIgnorePath /
  isHardcodedIgnoredDirectory contracts

Default behaviour (no .gitnexusignore or no negation pattern) is
byte-identical to pre-#771. Users who want to index an auto-filtered
directory add a single !pattern line — no env var, no flag, no
re-install.

Closes #771.

* fix(ingestion): honour re-ignore rules after .gitnexusignore negation (#771)

When .gitnexusignore contains both `!__tests__/` and
`__tests__/generated/`, the parent negation previously short-circuited
and allowed the re-ignored child through. Consult `ig.ignores(rel)`
after `hasExplicitUnignore` so a more-specific rule in the same file
correctly re-ignores a subset — matching .gitignore's last-match-wins
semantics. Adds a compound-pattern test locking this in.
2026-04-23 13:49:06 +01:00
Sonu Verma
253f9cae37
feat(ingestion): make large-file skip threshold configurable (#1044)
* feat(ingestion): make large-file skip threshold configurable

The walker previously hardcoded a 512KB skip threshold, which silently dropped legitimate large source files (e.g. ~900KB hand-written Java service classes) during analysis with no way to override short of editing source.

Allow overrides via the GITNEXUS_MAX_FILE_SIZE env var (KB) — consistent with the existing GITNEXUS_NO_GITIGNORE / GITNEXUS_VERBOSE patterns — and a matching --max-file-size <kb> flag on gitnexus analyze.

- New utility getMaxFileSizeBytes() in core/ingestion/utils/max-file-size.ts parses the env var, falls back to the 512KB default for missing/invalid values, and clamps against TREE_SITTER_MAX_BUFFER (32MB) to keep the downstream parser safe.
- filesystem-walker.ts now resolves the threshold per call and drops the 'likely generated/vendored' editorial when the user has explicitly raised the limit.
- analyze CLI wires --max-file-size to the env var and echoes a one-line notice when the threshold is overridden, mirroring how --no-gitignore is handled.
- index.ts documents the new flag and env var under the analyze help text.
- Warnings for invalid or out-of-range values are emitted exactly once per distinct value to avoid log spam.

Tests:
- New test/unit/max-file-size.test.ts covers defaults, KB parsing, clamp-at-ceiling, invalid-input fallback + warn-once, and distinct-value warnings.
- test/integration/filesystem-walker.test.ts gains a 'large file skip threshold (#991)' block: 600KB fixture skipped by default, included under GITNEXUS_MAX_FILE_SIZE=1024, invalid values fall back and warn once, and the 'generated/vendored' suffix is only emitted under the default threshold.

Closes #991

* fix(cli): show effective clamped max-file-size in banner

Addresses the PR #1044 review finding: the startup banner printed the raw GITNEXUS_MAX_FILE_SIZE value rather than the clamped effective threshold, producing misleading telemetry when the value exceeded the 32 MB tree-sitter ceiling.

The banner is also suppressed when the effective threshold equals the default, removing log noise when operators explicitly set the value to the current default.

Extracted the logic into a new getMaxFileSizeBannerMessage() helper and pinned the behavior with unit tests covering default, raised override, invalid fallback, and above-ceiling clamp cases.
2026-04-23 11:07:37 +01:00
azizur100389
bd271da7b7
feat(cli): gitnexus remove <target> to unindex a registered repo by name or path (#664) (#1003)
* feat(cli): gitnexus remove <target> to unindex a registered repo by name or path (#664)

Add a `remove` CLI command that deletes the `.gitnexus/` index AND
unregisters a repo from the global registry (~/.gitnexus/registry.json),
addressing the lifecycle gap flagged in #664: previously users had to
cd into the repo to run `clean`, and there was no path-based or
alias-based remove for an already-deleted working tree.

- New command `gitnexus remove <target> [-f|--force]`. `<target>` is
  alias / basename-derived name / remote-inferred name / absolute path.
- New helper `resolveRegistryEntry(entries, target)` in repo-manager.ts
  with path > name precedence; throws RegistryNotFoundError or
  RegistryAmbiguousTargetError (typed, `kind`-discriminated).
- Atomicity mirrors `clean`: fs.rm first, then unregisterRepo; partial
  failures self-heal on next `listRegisteredRepos({ validate: true })`.
- Idempotent on unknown targets (exit 0 with warning) per the #664
  spec: "behave atomically and idempotently so retries are safe".
- `--force` uses `clean`-style confirmation-skip semantics — distinct
  from `analyze --force` (pipeline re-index); here there is no pipeline
  so no conflation.
- 7 new unit tests cover resolver precedence, case sensitivity,
  ambiguity, and not-found hints; 2 integration tests cover the real
  CLI -> registry -> filesystem chain including the --allow-duplicate-name
  (#829) ambiguity case.

* fix(cli): canonicalize repo paths so remove/register match across platforms (#1003 review)

Address review feedback from @evander-wang and @magyargergo on PR #1003
plus the Windows + macOS CI failure (same root cause).

Problem:
- macOS: /var is a symlink to /private/var. `path.resolve` does NOT
  follow symlinks, so a child running analyze in /var/folders/X stores
  /private/var/folders/X (realpath from OS cwd) but an outer caller
  passing the symlink form misses.
- Windows: GitHub runners surface tmpdirs in 8.3 short-name form
  (RUNNERA~1) while process.cwd() returns the long form (runneradmin).
  Same divergence.

Fix: new `canonicalizePath(p)` helper wraps `path.resolve` plus
`fs.realpathSync.native`, falling back to `path.resolve` when the path
doesn't exist (preserves idempotent-on-missing semantics needed by
`remove <unknown>`). Applied at 3 call-sites — registerRepo,
unregisterRepo, resolveRegistryEntry — canonicalising BOTH the input
and each stored `entry.path` at compare time. That last bit is the
backward-compat story: registries written by older versions
(pre-canonicalisation) still match correctly, so we don't need a
migration script.

Test side: the ambiguous-target integration test now reads the path
from the registry snapshot rather than passing the outer `repoA`
variable directly, so it exercises the registry contract regardless of
which path form the platform stores. 4 new unit tests cover the helper
(idempotent, fallback-on-missing, absolute-for-relative) plus the
backward-compat resolver path.

* fix(cli): store resolved (non-canonical) path, compare via canonicalizePath (#1003 CI)

Follow-up to c5eceba0. The previous commit canonicalised the repo path
at BOTH write-time AND compare-time in registerRepo — that expanded
Windows 8.3 short names (RUNNER~1) to long names (runneradmin) when
storing `entry.path`. Pre-existing #829 unit tests that assert
`path.resolve(err.existingPath) === path.resolve(tmpPath)` then broke
because `tmpPath` is still short-form (path.resolve doesn't expand
8.3) while `entry.path` was long-form (canonicalizePath does).

Fix: split storage from comparison.
- entry.path stores `path.resolve(repoPath)` — whatever form the
  caller passed. `list` output and error messages show the path the
  user typed.
- All compare points (existing-entry lookup in registerRepo, the
  collision guard, unregisterRepo, resolveRegistryEntry path tier)
  canonicalise BOTH sides via `canonicalizePath`. That is where the
  /var ↔ /private/var and RUNNER~1 ↔ runneradmin divergence actually
  matters.

Net effect: storage is tolerant (preserves user input), matching is
strict (canonical-vs-canonical). Pre-existing #829 tests stay green
because `err.existingPath` is unchanged from what `path.resolve` gives
back; the cross-platform CI failure from #1003 stays fixed because
every comparison path goes through `canonicalizePath`.

* fix(cli): refuse destructive fs.rm when registry storagePath isn't <repo>/.gitnexus (#1003 review)

Address @magyargergo's inline review finding on remove.ts:89 and the
sibling vulnerability in clean.ts --all (caught during a pre-commit
safety audit). ~/.gitnexus/registry.json is a user-writable plain-text
file, so a corrupted or hand-edited entry could point storagePath at
the repo root (catastrophic: rm the working tree), an empty string
(→ cwd), a parent dir, or anywhere else. fs.rm(recursive: true,
force: true) on any of those is a runtime disaster.

- New UnsafeStoragePathError + exported assertSafeStoragePath() in
  repo-manager.ts. Pure lexical string check (Windows-case-
  insensitive) asserting entry.storagePath === path.join(entry.path,
  '.gitnexus').
- Guard wired into BOTH destructive registry-trusting sites:
  - remove.ts: exit 1 with actionable hint
  - clean.ts --all: skip the poisoned entry with a warning and
    continue (preserves existing per-repo error tolerance — one bad
    entry doesn't halt the batch)
- clean.ts default path and server/api.ts are safe-by-construction
  (they recompute storagePath from findRepo / getStoragePath rather
  than trusting the registry field).
- 8 unit tests cover the guard (valid, repo-root, parent, empty,
  unrelated, sibling, error payload, Windows case).
- 2 integration tests prove the full CLI path: remove-poisoned exits
  1 without touching the working tree; clean --all with a poisoned
  sibling entry cleans the good entry, skips the bad one, and leaves
  the poisoned repo intact.

* test(cli): assert full remove dry-run + success output shape (#1003 NIT)

Address the one NIT from the senior-reviewer pass on PR #1003: the
integration test was only checking for the "Run with --force" hint in
dry-run output, not verifying that the three actual console.log lines
(alias, repo path, storage path) appear. Same weak check on the
success-branch "Removed" output.

Tighten both assertions to toContain(alias), toContain(entry.path),
toContain(storagePath). Catches silent format regressions — e.g. a
future refactor that drops a console.log line or swaps
entry.name/entry.path in the output.

No code change; +20 test lines. All assertions in the happy-path
integration test now fire for a meaningful reason.
2026-04-21 11:52:59 +01:00
Sam Fakhreddine
c24bcc3bf1
fix: expose detect-changes in direct CLI (#892)
Squashed commits:
- test: fix risk_level mock case and prettier formatting in tool-direct-cli.test
- test: add edge-case coverage for detectChangesCommand formatter
2026-04-20 17:12:25 +01:00
azizur100389
dae7bd3b3f
feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry (#955) 2026-04-19 07:23:48 +01:00
Mr. WorldwideBrown
7c983d798f
Fix OpenCode config path, FTS extension load order, error messages, and CLAUDE.md stats (#781) 2026-04-11 06:18:14 +01:00
ivkond
4fed097abb feat(group): add sync pipeline, CLI, MCP tools, and monorepo fixture
Wire extractors into the sync pipeline with service boundary detection.
GroupService provides high-level API for all group operations.

- Sync pipeline: orchestrates extraction (HTTP, gRPC, topics) with
  service boundary assignment and exact matching
- GroupService: groupList, groupSync, groupContracts, groupQuery,
  groupStatus (groupImpact deferred to cross-repo follow-up PR)
- CLI: group create/add/remove/list/sync/contracts/query/status
- MCP tools: group_list, group_sync, group_contracts, group_query,
  group_status
- Monorepo fixture: 3 services (auth/orders/gateway) connected via
  gRPC + Kafka + HTTP — all intra-repo cross-links discovered
- Documentation: CLI commands and MCP tools added to both READMEs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 00:40:31 +03:00
Gabriel J Campbell
b03413dcf9
feat: added skip-agents-md cli flag (#517)
* feat: added skip-agents-md cli flag

* fix: apply prettier formatting

* feat: added skip-agents-md cli flag

* fix: apply prettier formatting

* feat: add skipAgentsMd option to skip AGENTS.md and CLAUDE.md updates

* fixed bad merge
2026-03-28 21:23:59 +00:00
Abhigyan Patwari
9f69c43100
feat(wiki): Azure OpenAI support for wiki command (#562)
* feat(wiki): extend LLMConfig/CLIConfig with Azure and reasoning model fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wiki): restore cursor model resolution, fix LLMProvider type, clean up regex

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(wiki): remove stale LLMProvider type alias from repo-manager

* fix(wiki): fix Azure auth header, api-version param, reasoning model params, content_filter error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wiki): tighten Azure detection, reasoning model regex, content_filter gating

- isReasoningModel: new regex matches only o1/o3 bare + any oN-mini/oN-preview; bare o4/o5/etc now return false
- isAzureProvider: use URL hostname matching to block spoofed subdomain URLs
- callLLM: warn on Azure legacy /deployments/ URL without api-version
- callLLM: gate content_filter error to azure===true; also catch ResponsibleAIPolicyViolation
- tests: add afterEach stub cleanup, spoofed-URL, bare-o4, non-Azure content_filter, and URL-only Azure auto-detect tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wiki): detect content_filter finish_reason in SSE stream and throw clear error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wiki): skip delta accumulation after content_filter, use provider-neutral error message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wiki): add Azure OpenAI option to interactive setup wizard

Inserts Azure as option [3] in the provider menu (shifting Custom to [4]
and Cursor to [5]), adds guided Azure setup flow with resource/deployment
prompts, v1/legacy URL format selection, reasoning-model flag, and
content_filter error handling in the catch block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wiki): store explicit false for non-reasoning Azure deployments, trim resource name inputs

- isReasoningModelDeployment now stores false (not undefined) when user says no
- Always include isReasoningModel in saved azureConfig (no conditional guard needed)
- Trim resourceName and deploymentName prompt inputs to avoid whitespace issues
- Improve reasoning model note to mention Azure requirement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wiki): add --api-version and --reasoning-model CLI flags for Azure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wiki): include apiVersion and reasoningModel in hasCLIOverrides guard

* fix(wiki): default provider to 'openai' in resolveLLMConfig when not configured

* style: apply prettier formatting

* fix(wiki): address PR review — remove unrelated files, harden inputs

- Remove evidence/, fix-adapter.js, and planning doc accidentally included
- URL-encode apiVersion in buildRequestUrl to prevent query string injection
- Add --no-reasoning-model flag to allow CLI override of saved config
- Simplify verbose ternary in Azure wizard prompt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(wiki): use execFileSync for EDITOR to prevent shell injection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 00:23:41 +05:30
Gergő Magyar
bf09eab95b
feat: configure prettier with pre-commit hook (#563)
* feat: configure prettier with pre-commit hook integration

Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo
root with husky pre-commit hook integration. Moves husky from
gitnexus/ to root package.json for reliable hook installation.

- Root package.json with prepare/format/format:check scripts
- .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4
- .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md
- .gitattributes enforcing LF line endings for Windows consistency
- Pre-commit hook uses direct node_modules/.bin/ paths (no npx)

* style: apply prettier formatting to entire codebase

One-time bulk format. No logic changes.
Use .git-blame-ignore-revs to skip this commit in git blame.

* chore: add .git-blame-ignore-revs for prettier format commit

* perf: pre-commit hook runs only tests related to staged files

Use vitest --related to scope test execution to tests that import
the changed files, instead of running the full suite on every commit.

* perf: remove vitest from pre-commit hook, keep in CI only

Pre-commit now runs lint-staged + tsc only. Tests run in CI
(ci-tests.yml) where they belong — keeps commits fast.

* ci: add prettier format check to quality workflow

PRs will now fail if code isn't formatted with prettier.
2026-03-28 14:58:04 +00:00
Zander Raycraft
6fabd7a2df
Merge pull request #402 from adonisdoda/feat/index-cli 2026-03-25 20:22:21 -05:00
chirag-nighut
fca7815c16 feat(wiki): add Cursor CLI as LLM provider option
Add Cursor headless CLI as a 4th provider option for wiki generation,
allowing users to leverage their Cursor subscription for wiki pages.

- Add --provider cursor flag and cursor-client.ts
- Add --review flag for interactive module tree editing
- Add --verbose flag for debugging
- Improve module tree generation (flatten single-child, unique slugs)
- Prevent DB timeout during long LLM calls

Usage: gitnexus wiki --provider cursor --model claude-4.5-opus-high
Made-with: Cursor
2026-03-25 11:39:37 +05:30
Shunsuke Hayashi
09e3609376 fix(analyze): address review — rename --no-git to --skip-git, fix stale cache
Addresses all review items from @magyargergo and Copilot:

1. **Rename --no-git to --skip-git**: Commander.js treats --no-X flags
   as negation of --X (stores as options.git = false, not options.noGit).
   --skip-git maps correctly to options.skipGit.

2. **Fix false " Already up to date\ on non-git folders**: When
 currentCommit is empty string, skip the cache check — we cannot
 detect changes without git, so always rebuild.

3. **Replace isGitRepo() with hasGitDir()**: Use filesystem check
 (statSync on .git) instead of shelling out to git CLI. Consistent,
 faster, and works when git is not installed.

4. **Fix misleading warning**: Message now only fires when .git
 directory is actually absent (not when git CLI fails).

5. **Add CLI integration tests**: Verify Commander maps --skip-git
 correctly and that non-git folders are rejected without the flag.
2026-03-22 17:40:02 +09:00
Shunsuke Hayashi
f0f384aab7 fix(analyze): address Copilot review — ESM import, CLI option, .gitignore guard
- Replace require(" fs\) with ESM-compatible top-level import (statSync)
- Register --no-git option in Commander CLI definition
- Use hasGitDir() instead of isGitRepo() for .gitignore update guard
 to match the PR intent (filesystem check vs git CLI invocation)
2026-03-22 16:24:21 +09:00
adonis
3567ef5794 feat(cli): add --allow-non-git option to index command and update tests 2026-03-20 19:46:47 -03:00
adonis
c1996f45d8 feat(cli): add 'index' command to register existing .gitnexus/ folder 2026-03-20 19:16:09 -03:00
Dmytro Semchuk
a7b8c302d4
Merge branch 'main' into feat/codex-support 2026-03-19 10:24:45 +01:00
ivkond
fbff6d08c0
feat(ingestion): respect .gitignore and .gitnexusignore during file discovery (#231)
* feat(ingestion): respect .gitignore and .gitnexusignore during file discovery

Add support for excluding files from indexing based on .gitignore and
.gitnexusignore patterns. Previously, GitNexus used only a hardcoded
ignore list, causing significant index pollution in repositories with
git-ignored directories containing code (e.g., Docker-mounted volumes).

Changes:
- Add `ignore` package for gitignore-spec pattern matching
- Add `loadIgnoreRules()` to parse .gitignore + .gitnexusignore
- Add `createIgnoreFilter()` returning glob-compatible IgnoreLike object
- Integrate filter into glob's `ignore` option for directory-level pruning
- Remove post-glob `.filter()` call (now handled during traversal)

The hardcoded DEFAULT_IGNORE_LIST remains as fallback for non-git repos.

Closes #228

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ingestion): address review feedback on ignore filtering

- Distinguish ENOENT vs EACCES in loadIgnoreRules (warn on permission errors)
- Add GITNEXUS_NO_GITIGNORE env var to bypass .gitignore parsing
- Fix bare-name pattern matching in childrenIgnored (check both with/without trailing slash)
- Rename isIgnoredDirectory to isHardcodedIgnoredDirectory for clarity
- Add clarifying comments for design decisions (D2 negation, D3 dot:false redundancy)
- Add tests for bare-name patterns, file-glob patterns, EACCES handling, env var

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ingestion): address second round of review feedback

- G1: Document GITNEXUS_NO_GITIGNORE in `analyze --help` and log when active
- G2: Add comment clarifying path-scurry POSIX normalization contract
- G3: Add IgnoreOptions interface — env var now falls back, callers can
  pass `noGitignore` explicitly for testability and future CLI flag
- G4: Add integration test verifying walkRepositoryPaths respects the env var

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(ingestion): gracefully skip files with unavailable tree-sitter grammars

Port unsupported language resilience from PR #301 by @jecanore.
- Make Kotlin import optional (like Swift) in parser-loader and parse-worker
- Add worker-local isLanguageAvailable() with filePath param for tsx distinction
- Track and log skipped files per language in both sequential and worker paths
- Add skippedLanguages to ParseWorkerResult for worker→main aggregation
- Add isLanguageAvailable unit tests

Refs: #301, #155, #228

Co-Authored-By: jecanore <juan@housingbase.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(e2e): add ignore + language-skip end-to-end test with fixture repo

Add a fixture repo (test/fixtures/ignore-and-skip-repo/) with .gitignore,
.gitnexusignore, TypeScript source files, and a Swift file to exercise
all three features end-to-end:

- File discovery: verifies .gitignore excludes data/ and *.log,
  .gitnexusignore excludes vendor/, source files are discovered
- Parsing: verifies TypeScript files produce Function nodes and DEFINES
  relationships, Swift files are skipped gracefully when grammar is
  unavailable

Add the test to the standalone group in ci-integration.yml and coverage job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): move ignore-and-skip-e2e test to e2e group per review feedback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): use temp directory instead of fixture for e2e ignore test

The fixture's .gitignore prevented data/seed.json and debug.log from
being committed — these files would be missing after checkout in CI.

Switch to creating the entire test structure in a temp directory via
beforeAll (matching filesystem-walker.test.ts pattern). This ensures
all files exist regardless of git ignore rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): correct graph API usage in e2e ignore test

Use graph.nodes property getter instead of graph.getNodes(), and check
Function node filePath instead of non-existent File nodes (File nodes
are created by processStructure, not processParsing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add workflows permission to ci-integration.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: change workflows permission to write per review

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: move workflows permission from ci-integration.yml to ci.yml caller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): fix Claude workflows for fork PRs, remove misplaced workflows perm

Three issues prevented Claude from running on fork PRs:

1. claude-code-review.yml lacked workflows:write — push failed when
   fork PRs modify .github/workflows/ files
2. claude.yml had no fork PR support — checked out main and couldn't
   fetch the fork's branch from origin
3. Cleanup step unconditionally deleted branches even when push failed,
   breaking the concurrent claude.yml workflow

Also removes workflows:write from ci.yml's integration job — CI tests
don't need that permission. The permission belongs on the claude
workflows that push fork branches.

Changes:
- Add workflows:write to both claude workflow permissions blocks
- Add fork PR detection + branch push/cleanup to claude.yml
- Add step id to push-fork; cleanup only runs if push succeeded
- Pass branch names via env vars to prevent shell injection (security)
- Add concurrency groups to prevent race conditions between workflows
- Remove misplaced workflows:write from ci.yml integration job

* fix(ci): use GitHub API for fork branch refs instead of git push

GITHUB_TOKEN cannot have 'workflows' permission — it's only valid for
PATs and GitHub Apps. This means git push fails whenever a fork PR
modifies .github/workflows/ files.

Replace git push with the GitHub REST API (POST/PATCH /git/refs) to
create temporary branch refs. The API creates a pointer to the
already-existing PR head commit without triggering the workflow file
push protection. Similarly, cleanup uses DELETE /git/refs instead of
git push --delete.

Also removes the invalid 'workflows: write' from permissions blocks.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: jecanore <juan@housingbase.io>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-03-16 13:26:20 +00:00
Zander Raycraft
03bfa3c4d9
FEAT: Added support for optional skill generation based on KuzuDB after initial repo analysis (npx gitnexus analyze --skills) (#171)
* calm fix 4 adding skills to repo [ISSUE #140]

* inspect

* unit and integration tests

* fixed hardcoded cohesion miss

* e2e tests for --skills flag for langauge/repo support

* Cohesion test e2e tests
2026-03-13 08:29:13 +00:00
Ryanba
1be910f54a
fix: skip unavailable native Swift parsers in sequential ingestion (#188)
* fix: skip unavailable native Swift parsers in sequential ingestion

* fix: warn when ingestion skips languages in verbose mode

* test: cover verbose skip warnings

* docs: update analyze flags

* docs: clarify verbose default

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-03-10 14:26:36 +00:00
Shockang
9d5ec5d19a
Improve MCP startup compatibility and lazy-load CLI commands (#207)
* Fix MCP startup transport compatibility

* Preserve CLI flags in MCP startup fix

* Harden MCP transport error handling

* Harden transport security and improve type safety

Transport hardening:
- Add MAX_BUFFER_SIZE (10 MB) cap to prevent OOM from oversized
  Content-Length or unbounded newline-delimited input
- Replace recursive readNewlineMessage with iterative loop to prevent
  stack overflow from consecutive empty lines
- Tighten looksLikeContentLength to require 14+ bytes before matching
- Add closed-state guard and error handling to send()
- Simplify processReadBuffer loop to break on error
- Fix loose equality (==) to strict (===)
- Widen constructor param types to ReadableStream/WritableStream

Type safety:
- Constrain createLazyAction generics so export name is validated
  against the module's actual exports at compile time
- Use proper type guard instead of lint suppression
- Fix test tsconfig type errors

Regression tests for all hardening fixes (13 tests passing).

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-03-07 07:47:09 +00:00
abhigyanpatwari
20ebd6b781 feat: security hardening, MCP improvements, skills, hooks, and CLI updates
- Export security primitives (CYPHER_WRITE_RE, isWriteQuery, isTestFilePath,
  VALID_NODE_LABELS, VALID_RELATION_TYPES) from local-backend
- Improve MCP kuzu-adapter with better query handling
- Add PR review skill for Claude, Cursor, and npm package
- Add CLI guide and CLI skills
- Update hooks for Claude plugin and Cursor integration
- Remove deprecated claude-hooks.ts CLI module
- Update eval-server, setup, and analyze CLI commands
- Improve CSV generator and ingestion processors
- Update CLAUDE.md and AGENTS.md configs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:13:42 +05:30
abhigyanpatwari
eb48c7352e fix: read CLI version from package.json instead of hardcoding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 16:32:28 +05:30
abhigyanpatwari
7fe8830402 merge: resolve conflicts with main for remote server mode (PR #66)
Keep main's barLog implementation, preserve both currentDbPath and
ftsLoaded reset in closeKuzu, take PR's new resolveRepo pattern
for /api/query. Path traversal guard confirmed intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:40:25 +05:30
Nico Prieto
f047a84d82 fix(server): restore security guards and error handling
Address code review feedback on the server-mode PR:

Critical fixes:
- Restore CORS whitelist (localhost + gitnexus.vercel.app only)
- Bind to 127.0.0.1 by default; add --host CLI flag for opt-in remote access
- Restore path traversal guard on /api/file (resolve + startsWith check)
- Restore try/catch on all route handlers + global error middleware
- Restore SIGINT/SIGTERM graceful shutdown handlers

Bug fixes:
- Add mutex to core initKuzu to prevent race conditions on concurrent
  DB switches (two requests for different repos no longer corrupt state)
- Track ftsLoaded flag and reset on DB switch / close so FTS extension
  is reloaded for each new database connection
- Restore input validation on /api/query (cypher required) and
  /api/search (query required)

Improvements:
- Add TTL-based cleanup for orphaned MCP sessions (30min idle eviction)
  to prevent memory leaks from network drops that skip onclose

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:42:00 +01:00
abhigyanpatwari
bef319491a feat(gitnexus): large-repo optimizations, multi-language support, bug fixes
Optimize ingestion pipeline for massive codebases (Linux kernel ~75K files):
- Worker pool: 8 threads, 1500 sub-batch, 30s per-batch timeout
- Streaming CSV generation with per-stream MaxListeners fix
- FileContentCache 3000 entries, resolveCache LRU eviction (20% at 100K cap)
- Leiden 60s timeout with single-community fallback
- SIGINT graceful shutdown, FTS dedup flag

Multi-language node support (Struct, Enum, Macro, Impl, Trait, etc.):
- Add 16 multi-language types to NodeLabel union
- Separate CSV writers with correct schema (no isExported)
- KuzuDB schema: backtick-escape reserved words (Macro, Union, Enum)
- Add all FROM/TO pairs for multi-language relationship edges

Fix bugs: getKuzuStats/deleteNodesForFile backtick escaping,
insertNodeToKuzu/batchInsertNodesToKuzu missing escaping + isExported,
progress bar flickering and timer display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:23:23 +05:30
Dmytro
984316d260 feat: add Codex MCP and skills support 2026-02-25 15:43:12 +01:00
abhigyanpatwari
302c7ba7f0 fix: disable embeddings by default, fix segfault on macOS/Linux
Embeddings (ONNX Runtime) caused segfaults on macOS (#38) and CUDA
install failures on Linux (#40). Since embeddings only power semantic
vector search in query() and BM25 + graph traversal already provide
strong retrieval, disable embeddings by default with minimal quality
impact.

Changes:
- Replace --skip-embeddings with --embeddings opt-in flag
- Remove disposeEmbedder() call (ONNX native cleanup segfaults)
- Add process.exit(0) when embeddings are used to bypass ONNX atexit hooks
- Suppress ONNX session warnings via logSeverityLevel
- Fix progress bar duplication by routing console output through bar.log()
- Add elapsed time indicator for long-running phases

Fixes #38
Fixes #40
2026-02-23 03:47:34 +05:30
abhigyanpatwari
79501abb6c fixed MCP concurrency issue with connection pool on kuzu 2026-02-17 16:31:05 +05:30
abhigyanpatwari
6ce715b62c repowiki CLI command implemented 2026-02-17 02:25:07 +05:30
abhigyanpatwari
eca55aacd7 fixed resource count multiplying issue ( using resource templates now ) 2026-02-13 21:28:36 +05:30
abhigyanpatwari
dda8de41a3 MCP and cli fixes 2026-02-07 06:21:21 +05:30
abhigyanpatwari
1735c0ffcd gitnexus wal cleanup preventing reindexing issue fixed 2026-02-06 06:35:15 +05:30