Commit graph

7 commits

Author SHA1 Message Date
Joseph Yared
b92c14cdd0
feat: add Factory AI (Droid) integration (#2543)
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
* feat(setup): add Factory Droid (MCP + skills) to gitnexus setup

Register 'droid' in the editor-targets abstraction so `gitnexus setup -c droid`
writes the MCP server to ~/.factory/mcp.json and installs skills to
~/.factory/skills/ from the single canonical skills/ source (no per-editor
copies). uninstall.ts is target-driven, so removal is covered automatically.
Adds unit + round-trip coverage.

* feat(plugin): add gitnexus-factory-plugin for droid plugin install

* docs: add Factory Droid to editor support table and setup docs

* fix(factory-plugin): guard augment hook against fan-out and DB contention

Reuse the Claude adapter's acquireHookSlot and LadybugDB owner probe
(bundled byte-identical, kept in lockstep by a drift test) instead of
running an unguarded augment. Add direct tests for the hook and manifests.

* docs: align Factory row in editor support table

* fix(factory-plugin): honor GITNEXUS_HOOK_CLI_PATH so augment runs on Windows

* docs(hooks): point bundled guard copies at their drift tests

* docs(factory-plugin): note the Execute tokenizer's quoting limit

* docs(readme): clarify the Full tier and group the Factory row

* docs(hooks): trim drift note to a single line

* test(ci): run factory-plugin tests on the windows cross-platform lane

* refactor(hooks): drop the drift-note comments, the tests already enforce it

* fix(factory-plugin): pin CLI version and parse quoted shell patterns

- Pin mcp.json and the hook's npx fallback to gitnexus@<version> from
  the plugin manifest, registered with the release sync script so a
  mutable @latest can never execute on MCP connect or augment fallback
- Port the #2938 shell tokenizer (tokenizeShellWords + parseRgGrepPattern)
  so quoted, backslash-escaped, --regexp=, -eVALUE, and -- patterns survive
- Add the #2938 regression matrix and pin assertions to factory-plugin.test.ts

* docs: add Factory Droid to published npm README

* fix(factory-plugin): wire marketplace so droid installs the Factory plugin

Add .factory-plugin/marketplace.json sourcing ./gitnexus-factory-plugin.
Droid reads it before .claude-plugin/marketplace.json, so
`droid plugin install` now delivers the Factory plugin (Execute matcher,
pinned mcp.json) instead of the translated Claude plugin (Bash matcher,
gitnexus@latest). Register the surface in the version-sync script and
cover the wiring in the factory and sync test suites.

* fix(factory-plugin): use registry lookup for index resolution

Bundle registry-query.cjs so external indexes resolve (#3060); re-pin to 1.6.12.

* fix(factory-plugin): sync Execute parser with Cursor hook

Fixes echo-rg and -f false positives; tighten test env isolation.

* fix(factory-plugin): stop no-match augment from re-running via npx

A PATH `gitnexus` that finds no match exits 0 with empty stderr, which
fell through to a second `npx -y gitnexus@<pin> augment` with its own 8s
timeout (16s worst case vs the 10s hook budget). Fall through to npx only
when the PATH launcher is missing (ENOENT); any launched PATH binary,
including a timeout or non-zero exit, now ends the augment.

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

* fix(factory-plugin): filter augment stderr to the [GitNexus] block

runAugment returned raw child stderr, so npm/Node/LadybugDB warnings leaked
into additionalContext and noise-only stderr counted as success. Port the
Claude adapter's extractAugmentContext (verbatim, with isDebugEnabled) and
apply it on every launch tier before the success decision. Adds a drift test
against the Claude copy and PATH-tier noise/noise-only behavior tests.

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

* fix(factory-plugin): quote DROID_PLUGIN_ROOT in hook command

An unquoted plugin root containing spaces (e.g. a Windows user profile
path) split into multiple argv words, so the PostToolUse hook silently
never ran. Quote it like the Claude plugin does, and pin the exact
quoted command in the hooks.json wiring test.

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

* fix(release): stage Factory plugin manifests in the release commit

The rc release job stages only the original four manifest surfaces in the
detached release commit, so the v<version> tag tree carried the Factory
plugin.json, mcp.json and marketplace.json at the previous version while
--check (working tree) passed. Stage them too, and guard the git add block
against the synced surfaces in sync-plugin-manifests.test.ts.

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

* refactor(factory-plugin): simplify hook gates, spawn tiers and tests

- main(): resolve the repo only after the tool-name and pattern gates,
  matching the Claude/Cursor hook order (skips fs/git work on no-op calls).
- runAugment(): share one spawnAugment helper between the
  GITNEXUS_HOOK_CLI_PATH and npx tiers; PATH tier ENOENT logic unchanged.
- factory-plugin test: pre-filter comment lines instead of `continue`.
- sync-plugin-manifests test: hoist EXECUTABLE_MCP_FILES and derive
  TOTAL_SURFACES from its length.
- fnSource(): throw when the function or its closing brace is not found.

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

* fix(cli): list Factory Droid in localized setup help

`localizeCliHelp` overwrites the `setup` command description with the
`help.command.setup.description` i18n key, so the literal edited in
index.ts never reached `gitnexus setup --help`. Add Factory Droid to the
en and zh-CN keys.

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

* Address PR review feedback (#2543)

- factory hook: run every augment tier under the bundled Unix timeout
  guard (npx tier group-kills), keeping exactly-one-tier fall-through
- hook-db-lock-probe: trim GITNEXUS_HOOK_{LSOF,PS}_PATH once so a padded
  override is used, not silently replaced (all 3 copies)
- hook-lock: evict a stale slot via rename-to-tombstone + identity check,
  so a concurrently recreated fresh lock is never deleted (all 4 copies)
- registry-query: a set-but-invalid storage override resolves no repo
  instead of falling back to the registry storagePath (all 4 copies)
- publish.yml: stage the ten skill mcp.json manifests in the rc release
  commit; the staging test now requires every synced surface

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

* Address PR review feedback round 2 (#2543)

- hook-lock: replace rename-to-tombstone eviction with an O_EXCL per-slot
  `.evicting` marker plus an identity re-check before unlink, so a live
  lock is never moved, and a crashed evictor leaves only a self-expiring
  marker (all 4 copies)
- hook-db-lock-probe: clamp GITNEXUS_HOOK_PROC_CMDLINE_MAX to a named
  256 KiB ceiling and require an integer, so an oversized override can
  no longer fail the buffer allocation and miss a live owner (all 3 copies)
- registry-query: treat an empty GITNEXUS_STORAGE_PATH/ROOT as set but
  invalid, matching the CLI's `!== undefined` rule (all 4 copies); the
  factory test env now deletes those keys instead of blanking them

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

* Address PR review feedback round 3 (#2543)

- hook-db-lock-probe: a capped /proc cmdline read stops early only once
  both the GitNexus token and the mcp/serve mode are present (or at EOF,
  the ceiling, or the budget), so a mode word such as `--require mcp`
  before the GitNexus path no longer hides a live owner (all 3 copies)
- registry-query: correct the override comment; a filesystem root is
  invalid only for GITNEXUS_STORAGE_PATH, not GITNEXUS_STORAGE_ROOT
  (all 4 copies, comment only)

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

* Address PR review feedback round 4 (#2543)

- hook-db-lock-probe: an fd-directory read error other than ENOENT or
  ENOTDIR on an identified server candidate now fails closed ('timeout')
  instead of reporting not-owned (EMFILE/ENFILE/ENOMEM/EINTR)
- hook-db-lock-probe: resolve GITNEXUS_HOOK_TIMEOUT_PATH to an absolute
  path before validating and caching it, so callers that spawn with a
  request cwd can still execute the guard
- hook-db-lock-probe: document the chunked cmdline read's actual stop
  conditions (all 3 copies)

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

* Harden hook-lock eviction marker lifecycle (#2543)

Per the chosen option (B) for the stale-slot eviction race:
- `.evicting` markers carry a per-call owner token (pid + random hex)
- an evictor re-reads its token immediately before the slot identity
  check and unlink; a stalled evictor whose marker was broken backs off
- `finally` removes the marker only while it still holds our token
- an orphaned marker is broken only if, re-checked just before unlink,
  its bigint identity and token are unchanged from when judged stale
- doc comment states the two remaining two-syscall windows (slot
  lstat->unlink, marker token->unlink); POSIX has no conditional
  unlink, and the worst case is one extra concurrent augment

All four byte-identical hook-lock copies updated.

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

* Fix CodeQL file-system race in hook-lock orphan-marker check (#2543)

breakOrphanedMarker stat'd the marker by path and then read it by path,
which CodeQL flags (js/file-system-race): the file could be replaced
between the two calls. Take the stat and the token from one open
descriptor (readMarkerSnapshot, O_NOFOLLOW where available) for both the
"judged stale" snapshot and the pre-unlink re-check. All four hook-lock
copies updated.

The replaced-marker test injected its swap via a readFileSync(path) spy,
which no longer fires; it now swaps the marker just before its second
open, counting opens of the marker path only (a per-path counter fired
early on slot-0 and let a mutant pass).

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

* Unregister hook-lock exit listener on release (#2543)

Each acquireHookSlot registered `release` as a process 'exit' listener
that was never removed, so a long-lived process acquiring and releasing
slots repeatedly would accumulate listeners (MaxListenersExceededWarning)
and retain every closure. release() now removes itself. All four
hook-lock copies updated; a test asserts 12 acquire/release cycles leave
the 'exit' listener count unchanged.

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 19:12:40 +01:00
azizur100389
fff01189b1
fix(cpp-hooks): handle pack-base comments and missing hook overrides (#2247) 2026-06-18 21:55:46 +01:00
Minidoracat
912285064a
perf(hooks): cmdline-first Linux db-lock scan, drop the lsof fallback (#2180) (#2183)
* perf(hooks): cmdline-first Linux db-lock scan, drop the lsof fallback (#2180)

The probe's Linux scan was O(processes × fds) — stat every fd of every
process — so on a busy host it blew its budget and fell through to lsof,
which then timed out (~2 s) and fail-closed. Every Grep/Glob/Bash hook
spent ~2 s of CPU to conclude 'couldn't tell'.

Rewrite linuxProcScanFindGitNexusServer (name kept; return type now
tri-state 'owned' | 'not-owned' | 'timeout') as three phases:
  0. /proc/<pid>/comm prefilter — kernel task->comm, never touches the
     target's memory maps; truncation-safe whitelist match (comm is
     capped at 15 visible chars). Calibrated to what a real server
     reports: @ladybugdb/core's worker_threads rename the main thread to
     'MainThread', so that is whitelisted alongside the launcher
     basenames — omitting it would blind the probe to every server.
  1. bounded /proc/<pid>/cmdline read (openSync+readSync, default 16 KiB
     with a floor of 4 KiB and a bounded escalation up to a hard ceiling)
     so a D-state holder cannot stall the hook and the mcp/serve mode
     token is never clipped off a long interpreter path.
  2. dev+ino fd match for the 0–2 survivors only.

Dispatch: 'owned' and 'timeout' both map to true. Timeout is now
fail-closed (overload self-throttle) instead of falling through to lsof;
the Linux lsof fallback is removed entirely. End-to-end semantics on
busy hosts are unchanged (the old lsof arm also fail-closed there) — the
~2 s of wasted work and the orphan-spawning lsof are what's gone.
macOS lsof+ps and Windows Restart Manager paths are untouched.

Also: fix the budget parse bug (Number(raw && trim()) treated '0' as
1200; now parseInt-then-validate, with <= 0 an explicit immediate
timeout) and add GITNEXUS_HOOK_PROC_ROOT so the Linux scan can be unit
tested against a fixture procfs instead of the host's real /proc.

Measured on a 583-process host with 6 background gitnexus mcp servers:
owner detection 6–12 ms (was ~1216 ms + lsof timeout), ~100x.

Tests: new hook-db-lock-probe.test.ts drives all three phases against a
fake procfs (comm-truncation safety, Phase 0 trap, 4 KiB-boundary
owner-miss guard, budget=0 immediate timeout, EACCES fail-closed) plus a
live-/proc e2e that pins the fd-visible lbug-handle property against a
real subprocess holder. The lsof/ps owner-detection suites are relaned
to macOS (Linux no longer takes that path); the lsof orphan-reaping
suite is removed (no lsof is spawned on Linux now) with a rationale note.

Note: pre-commit typecheck skipped; remaining tsc errors are pre-existing
on main (none in files touched here).

* fix(hooks): honest EACCES verdict + real escalation coverage (#2183 review)

Addresses the tri-review (maintainer + Codex):

- [P2] Phase-2 fd-dir EACCES no longer claims 'owned'. /proc/<pid>/fd is
  owner-only (0500), so a cross-user/root gitnexus server serving ANY
  repo cleared Phase 0+1 and hit EACCES here, and the old catch returned
  'owned' — falsely claiming it locks THIS repo's lbug (dev+ino never
  compared) and permanently suppressing augment. Split the failure
  shapes: ENOENT -> continue (raced away); EACCES/EPERM and transient
  EIO/ESTALE -> 'timeout' (unverifiable -> fail-closed, but honest, not a
  false ownership claim); ENOTDIR/other structural errors -> continue
  (not a real fd dir). Same fail-closed dispatcher outcome, no false
  'owned', plus a GITNEXUS_DEBUG diagnostic so an operator can tell this
  skip path from a real owner.
- [P2] The escalation test now actually iterates the escalation loop:
  the gitnexus token sits under 4 KB while the mode token is padded past
  GITNEXUS_HOOK_PROC_CMDLINE_MAX=4096, and a readSync spy asserts >1 read
  (the old 9 KB-under-16 KB-cap shape read once and never escalated).
- escalation loop now re-checks the budget each iteration and returns a
  distinct timeout sentinel (never '' — an empty string would read as
  'not a candidate' and could drop a real owner -> fail-open); the caller
  maps it to 'timeout'.
- GITNEXUS_HOOK_PROC_ROOT is gated to test context so a stray production
  env export can't disable Linux owner detection (fail-open).
- New uid-agnostic spy tests pin every fd-readdir errno branch
  (EACCES/EPERM/EIO/ESTALE -> timeout, ENOTDIR -> not-owned) regardless
  of the runner's uid (the disk chmod-000 tests no-op under root).

Note: pre-commit typecheck skipped; remaining tsc errors are pre-existing
on main (none in files touched here).

* fix(hooks): drop the always-true outOfBudget presence guard (CodeQL #2183)

CodeQL flagged `typeof outOfBudget === 'function' && outOfBudget()` as
unneeded defensive code: readLinuxCmdline has a single caller
(linuxProcScanFindGitNexusServer) that always passes the callback, so
the typeof guard is dead. Drop it, leaving `if (outOfBudget())`, and note
the invariant in the comment. Mirrored in the byte-identical plugin copy.

* fix(hooks): parse numeric hook env with Number() so scientific notation works (#2183 review)

getCmdlineMaxBytes and resolveLinuxProcBudgetMs parsed their env via
Number.parseInt(raw, 10), so a value like "16e3" silently became 16 (parseInt
stops at 'e') instead of 16000. Switch both to Number(String(raw).trim()),
which honors scientific notation and is stricter on trailing garbage
("123abc" -> NaN -> default) — matching the repo-majority Number()+isFinite
env idiom (src/cli/analyze.ts, src/core/embeddings/hf-env.ts).

The two functions had DIFFERENT guard skeletons, so a verbatim swap would
regress the budget: resolveLinuxProcBudgetMs used `raw != null ?` with no
empty-string short-circuit, and Number("")===0 (vs parseInt("")===NaN) would
make a set-but-empty GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS="" resolve to budget 0
=> immediate fail-CLOSED timeout => augment permanently skipped. Added the
`&& String(raw).trim()` guard so ''/whitespace fall to the 1200 default while
"0" still parses to the deliberate #2180 immediate-timeout vector.

Exported both helpers for white-box tests (the values are otherwise only
observable indirectly through scan timing) and added platform-independent
coverage: "16e3"->16000, ""/whitespace->1200 (the regression guard), "0"->0,
"123abc"/unset->1200, cmdline "8e3"->8000, "2e3"/""/unset->16384.

Both byte-identical hook-db-lock-probe.cjs copies updated together.

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

* fix(hooks): allocUnsafe the per-chunk cmdline read buffer (#2183 review)

readLinuxCmdline allocated each per-chunk read buffer with Buffer.alloc(chunkCap),
zero-filling memory that readSync immediately and fully overwrites. Switch the
hot read buffer to Buffer.allocUnsafe — safe because readSync initializes
exactly [0, bytes), only buf.subarray(0, bytes) is consumed, and Buffer.concat
deep-copies that slice into `collected`, so the uninitialized tail can never
reach the decoded cmdline. The zero-length `collected = Buffer.alloc(0)` is left
unchanged (allocUnsafe gains nothing on a 0-length buffer). The existing D3
multi-chunk decode tests cover the read path and stay green.

Both byte-identical hook-db-lock-probe.cjs copies updated together.

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

* test(hooks): harden the live /proc owner-detection e2e against CI flake (#2183 review)

Two flake mechanisms, fixed without weakening what the e2e proves:

- Holder readiness (the genuine false-FAIL): the pid-file poll was 200x25ms=5s;
  a loaded runner can be slow to spawn the child, tripping
  expect(holderPid).toBeGreaterThan(0). Widened to ~10s and raised the per-test
  timeout 20s -> 40s.
- Scan budget (kept the assertion honest): the live scan ran at the default
  1200ms. Because the dispatcher maps a budget 'timeout' to owned=TRUE, a busy
  host exhausting 1200ms before reaching the holder would make the assertion
  pass for the WRONG reason (a hollow timeout, not real fd-visible detection).
  Set a generous explicit 10000ms budget via the existing setEnv() helper so the
  module afterEach restores it (replacing the raw `delete process.env...` that
  bypassed env tracking). Raised the coarse timing regression guard to sit ABOVE
  the budget (5000 -> 15000) so a legitimately-slow-but-correct scan can't trip
  it.

The load-bearing asserts (dev+ino fd-visibility precheck, owned===true for our
own lbug) are unchanged. Verified the e2e executes (not skipped) on Linux.

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

* chore(changelog): empty the root CHANGELOG [Unreleased] section

Per maintainer request, nothing should sit under [Unreleased] in the root
CHANGELOG.md (the release-owned changelog is gitnexus/CHANGELOG.md, whose
[Unreleased] is already empty). Removes all three accumulated blocks — Fixed
(#2163), Performance (#2180), Changed (KuzuDB->LadybugDB) — leaving only the
[Unreleased] header above [1.5.3]. Pure removal; no release sections touched.

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:52:14 +01:00
Minidoracat
0054496323
fix(hooks): wrap the augment CLI child in the orphan guard (#2163) (#2169)
Some checks failed
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
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (push) Has been cancelled
* fix(hooks): wrap the augment CLI child in the orphan guard (#2163)

Follow-up invited by the maintainer on #2165: the augment child
(7s local / 12s npx) was the longest-lived unwrapped subprocess, exposed
to the same SIGKILL-orphan mechanism fixed for lsof/ps.

- Export resolveUnixGuardTimeout from the probe module (both copies,
  byte-identical); adapters share the same module instance, so the memo
  and lazy self-test still run at most once per hook process.
- Wrap every CLI-executing branch of runGitNexusCli in the three
  probe-equipped adapters with the guard: budget ceil(inner/1000)+1
  seconds with -k 1, strictly above each branch's inner spawnSync
  timeout, so the supervised path is unchanged and the wrapper only
  matters once the hook itself is SIGKILLed. Windows and no-guard hosts
  keep byte-identical argv. The plugin adapter's PATH-direct gitnexus
  branch (its most common production path) is wrapped too; the cheap
  which/where probe is not.
- Cursor integration: debug-gated 'augment skipped: hook slots
  saturated' on the slot-starved early return. Its augment child stays
  unwrapped for now — that integration does not install the probe
  sibling (the 'cursor probe' item on the #2163 follow-up list).
- Reaping tests get a guard-availability precheck with an explicit
  failure message (assertion, not skipIf, so a coreutils-less Linux
  host fails diagnosably instead of going silently green).
- Tests: orphaned-augment reaping (CJS + Plugin, red without the wrap,
  ~9.1s reap measured), disabled-sentinel degradation equivalence,
  source pinning for all three adapters (exact per-branch budget-formula
  counts) + probe export + cursor debug line.

Note: pre-commit typecheck skipped; remaining tsc errors are
pre-existing on main (none in files touched here).

* fix(hooks): group-SIGKILL the npx arm, prove guard exit propagation (#2169 review)

Addresses the tri-review findings on #2169:

- [P2] npx-arm containment: the CLI is the guard's grandchild there —
  at budget expiry coreutils timeout TERMs the group, npx (the obedient
  direct child) dies, timeout returns, and -k never fires, so a
  SIGTERM-immune grandchild escaped unbounded. The npx arm's wrapper now
  uses -s KILL: an unignorable group SIGKILL at budget that reaps the
  grandchild (kept -k 1 as a harmless belt; direct-exec arms keep
  TERM-first). CHANGELOG, adapter docblocks, and the test comment now
  state the per-arm semantics honestly. New behavioral test: a staged
  hook with a PATH-injected fake npx spawning a SIGTERM-immune
  grandchild is SIGKILLed; the grandchild must be reaped (red without
  -s KILL), with a route self-proof marker pinning the npx arm.
- [P3] guard self-test now proves exit-status propagation
  (sh -c 'exit 42' must yield status 42), so an always-exit-0 stub like
  /bin/true is rejected and resolution falls through to the built-in
  candidates instead of silently killing the augment feature. New test:
  stub guard rejected, augment still emits context.
- [P3] cleanup SIGKILLs in the reaping tests re-check the
  /proc/<pid>/cmdline identity immediately before firing (PID-reuse
  guard), applied consistently to the two pre-existing #2165 spots and
  both new tests.
- Review notes: source pins now constrain wrapper argv order and exact
  per-arm counts; adapters degrade to unwrapped on probe version skew
  (typeof check) instead of a swallowed TypeError; export JSDoc wording
  fixed for relative env paths; debug-gated diagnostic when no guard is
  available (e.g. macOS without coreutils), with the CHANGELOG entry
  qualified accordingly.

Note: pre-commit typecheck skipped; remaining tsc errors are
pre-existing on main (none in files touched here).

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-12 15:17:30 +01:00
Minidoracat
10d1e47df3
fix(hooks): bound db-lock probe subprocesses and gate probe behind hook slot (#2163) (#2165)
* fix(hooks): bound db-lock probe subprocesses and gate probe behind hook slot (#2163)

The Claude PreToolUse db-lock probe leaks orphaned lsof processes when
the hook process is hard-killed mid-probe (e.g. Claude Code's 10s hook
timeout under load). Orphans accumulate, raise load, slow the next
probe, and snowball to sustained 100% CPU.

- Wrap the unix lsof/ps fallback in coreutils timeout (-k 1 2 / -k 1 1),
  resolved via a lazy self-test, so probe children self-destruct within
  ~3s even if the hook is SIGKILLed. GITNEXUS_HOOK_TIMEOUT_PATH
  overrides the guard binary; the sentinel value 'disabled' turns the
  guard off; hosts without a usable guard keep the previous behavior.
- Acquire the per-repo hook slot before probing (all three adapters),
  bounding concurrent probes to 3 per .gitnexus, with probe and augment
  inside try/finally so the slot is always released.
- Tests: source-order contract, slot-gating behavior, orphan reaping
  with a SIGTERM-immune fake lsof and a SIGKILLed parent (red on base),
  probe-copy byte parity, no-guard equivalence, broken-guard rejection.

Note: pre-commit typecheck skipped; the 62 tsc errors are pre-existing
on main (all in src/core/** and src/server/, none in files touched
here; base==head invariant verified).

* fix(hooks): address tri-review P3 findings (#2165)

- Map guard signal-death (status null + signal, no spawnSync error) to
  fail-closed at both the lsof and ps call sites, closing the freeze
  window (SIGSTOP / laptop sleep > 2s) that previously landed fail-open.
  Rewrite the exit-code comments: coreutils surfaces the -k kill as
  signal death, 124 is budget expiry (live arm), 137 covers only
  exit-code-propagating wrappers or an externally SIGKILLed child.
- Add a debug-gated 'augment skipped: hook slots saturated' stderr line
  on the slot-starved early return in all three adapters, restoring
  observability under GITNEXUS_DEBUG=1.
- GITNEXUS_HOOK_TIMEOUT_PATH now participates in candidate fall-through:
  the env candidate is tried first, then the built-ins, each behind the
  lazy self-test — an existing-but-unusable env path (directory,
  non-executable) can no longer silently disable orphan containment.
- Tests: +6 — guard exit 124 pins the live arm (CJS+Plugin), guard
  signal-death pins the new mapping (CJS+Plugin, red before the fix),
  antigravity behavioral slot-gate, env-dir fall-through still reaps a
  SIGTERM-immune orphan via a built-in guard.

Note: pre-commit typecheck skipped; the 62 tsc errors are pre-existing
on main (none in files touched here).
2026-06-11 15:38:13 +01:00
ManniX-ITA
39e9b40136
fix(windows): pass windowsHide:true to every child_process spawn-family call (#1794)
* fix(hooks): pass windowsHide:true to every spawnSync to suppress flashing console windows on Windows

On Windows, every PostToolUse and Stop event from Claude Code (and
the Cursor integration variant) cold-spawns ``node`` / ``npx.cmd`` /
``git`` / ``lsof`` through ``child_process.spawnSync``. Without
``windowsHide: true`` in the options, Node's child_process module
asks ``CreateProcess`` to use ``STARTF_USESHOWWINDOW`` with
``SW_SHOWDEFAULT``, and a black console window flashes onto the
user's desktop for the duration of the call. Under active
editor / agent use this means a near-continuous stream of pop-up
windows — unusable in practice (reported live on a Windows 11
workstation running the gitnexus Claude plugin against an active
project; the flashes stack on the taskbar and steal focus from the
editor).

The Node fix is one option flag per spawnSync:

    spawnSync(cmd, args, {
        encoding: 'utf-8',
        timeout,
        cwd,
        stdio: ['pipe', 'pipe', 'pipe'],
        windowsHide: true,            // <-- new
    });

``windowsHide`` is a no-op on macOS/Linux (Node docs: "Hide the
subprocess console window that would normally be created on Windows
systems"), so the patch is platform-neutral and zero-risk on the
other two majors.

This commit touches every ``spawnSync`` call in the three sources
that ship the hook layer:

* gitnexus/hooks/claude/gitnexus-hook.cjs            (4 sites)
* gitnexus/hooks/claude/hook-db-lock-probe.cjs       (3 sites)
* gitnexus-claude-plugin/hooks/gitnexus-hook.js      (6 sites)
* gitnexus-claude-plugin/hooks/hook-db-lock-probe.cjs (3 sites)
* gitnexus-cursor-integration/hooks/gitnexus-hook.cjs (3 sites)

Total: 19 spawn sites guarded. ``hook-lock.cjs`` / ``hook-lock.js``
don't spawn subprocesses; nothing else in the hooks/ dirs touches
``child_process``.

Verified on Windows 10 22H2 / Node 22.21 / gitnexus 1.6.5 by
installing the locally-built tarball and running an active Claude
Code session against a large mixed-language repo — no console
window appears for any hook fire (pre-fix: ~2-3 visible flashes per
edit). No behavioural change on Linux/macOS hosts.

* test(hooks): regression — every hook spawnSync paired with windowsHide:true

Source-level assertion that every ``spawnSync`` invocation in the
hook layer has a matching ``windowsHide: true`` in its options
object. Without the flag, Node's child_process module asks
CreateProcess to use STARTF_USESHOWWINDOW with SW_SHOWDEFAULT and
a black console window flashes onto the user's desktop for the
duration of each call — see the parent fix commit.

The check is source-level rather than behavioural because:

* the flag's effect is observable only on Windows;
* GitHub Actions runs vitest on Linux for the hook tests;
* regressing this is easy (every new spawnSync site has to remember
  to add the flag), and a runtime check on a Windows-only CI leg
  would still let a PR land on the main branch first.

Counts spawnSync occurrences and windowsHide:true occurrences per
file (in code, ignoring comments) and asserts equality. Five files
covered:

* gitnexus/hooks/claude/gitnexus-hook.cjs
* gitnexus/hooks/claude/hook-db-lock-probe.cjs
* gitnexus-claude-plugin/hooks/gitnexus-hook.js
* gitnexus-claude-plugin/hooks/hook-db-lock-probe.cjs
* gitnexus-cursor-integration/hooks/gitnexus-hook.cjs

Adding a new hook file requires updating the HOOK_FILES tuple. A
sanity assertion ``spawnCount > 0`` catches accidental deletion of
all spawn calls in a future refactor (would otherwise silently make
the count-equality assertion trivially true).

Sits next to the existing "no shell: true" and ".cmd extension"
regression tests in test/unit/hooks.test.ts — same shape, same
spirit.

* fix(src): extend windowsHide:true to every spawn-family call in cli/core/mcp/server

Companion to the hook-layer fix in this branch's first commit. The
same Windows console-window flash bug applies to every
``spawn`` / ``spawnSync`` / ``execFile`` / ``execFileSync`` /
``execFileAsync`` / ``execSync`` call in the source tree — not just
the hooks. The MCP local backend
(``src/mcp/local/local-backend.ts``) and the ``gitnexus serve`` git
helpers (``src/server/git-clone.ts``) are particularly bad because
they run from daemonized processes that have no parent console; the
spawned child auto-allocates one and it pops onto the user's
desktop. The CLI sites are less visible (the user is at a terminal
with an existing console; ``stdio: 'inherit'`` shares it) but the
flag is harmless there — windowsHide only suppresses NEW console
allocation, an inherited parent console is untouched. The visible
output of ``gitnexus analyze`` and friends is preserved verbatim.

The pre-existing fix at ``src/core/lbug/extension-loader.ts:96``
established the convention in this codebase. This commit applies it
uniformly.

Sites covered (21 new):

| File | Sites |
|---|---|
| src/cli/analyze.ts           | 1 |
| src/cli/setup.ts             | 2 |
| src/cli/wiki.ts              | 3 |
| src/core/embeddings/embedder.ts | 1 |
| src/core/git-staleness.ts    | 3 |
| src/core/run-analyze.ts      | 1 |
| src/core/wiki/cursor-client.ts | 2 |
| src/core/wiki/generator.ts   | 3 |
| src/mcp/local/local-backend.ts | 2 |
| src/server/git-clone.ts      | 2 |
| src/core/lbug/extension-loader.ts | (already had it, untouched) |

Combined with the 19 hook sites from the first commit + the 1
pre-existing extension-loader site, the codebase now has uniform
``windowsHide: true`` on every spawn-family call.

Behavioural notes:

* ``windowsHide`` is documented by Node as a no-op on POSIX —
  Linux/macOS hosts see byte-identical behaviour.
* ``stdio: 'inherit'`` callers (e.g. ``cli/wiki.ts:522`` opens the
  editor in the user's terminal) keep their interactive UX. The
  child inherits the parent's stdio handles; no new console is
  allocated; the flag has nothing to hide.
* Piped callers (``stdio: ['pipe',…]``) continue to deliver every
  byte of stdout/stderr back to the parent for the parent to log
  / process / re-print. No output is swallowed.
* ``execSync`` / ``execFileSync`` callers that previously had no
  ``stdio`` option (e.g. ``generator.ts:887`` ``execSync('git
  rev-parse HEAD', { cwd })``) keep their default pipe semantics
  (``.toString()`` still works) — windowsHide is added alongside
  the existing ``cwd`` option.

Verified on Windows 10 22H2 / Node 22.21 by installing the locally
built tarball and exercising:

* MCP detect_changes via the local backend → no flash.
* gitnexus serve → no flash on git clone/clone-pull.
* gitnexus analyze interactively → output appears in terminal as
  before, no extra window.

* test(windowsHide): extend regression to every spawn-family call in src/

Companion to the src/ patch. The hooks.test.ts regression now
covers 16 files (5 hooks + 11 source files), and asserts the
invariant for every spawn-family function — not just spawnSync.

Changes:

* Generalise countSpawnCalls() to also count spawn, execFile,
  execFileSync, execFileAsync, execSync (the entire spawn-family
  surface of child_process). Skip method calls (e.g. RegExp.exec)
  via a negative-lookbehind on ``.``.
* Add SRC_FILES table with all 11 source-tree files that import
  spawn-family functions from child_process.
* Loop over [...HOOK_FILES, ...SRC_FILES] so a regression in any
  file fails the same test name.
* Tighten the assertion to ``hideCount >= spawnCount`` rather
  than strict equality, because some sites (e.g. setup.ts:534
  using execFileAsync via shell:true on Windows) may legitimately
  add windowsHide to nested option objects in future refactors.
* Sanity gate ``spawnCount > 0`` per file catches a refactor
  that deletes all spawn calls (would otherwise make the
  assertion trivially true).

Manually exercised against the patched repo:
  16 files, 28 total spawn-family calls, 28 windowsHide:true.
  All pass.

The convention to keep this list in sync: every new file in
gitnexus/src/ that imports from 'child_process' must be added to
the SRC_FILES tuple. The cost is one line per file; the benefit
is the next contributor never has to think about windowsHide
again — the test will catch a miss before merge.

* style: prettier --write on storage/git.ts + hooks.test.ts

CI quality / format job flagged two formatting issues in the
merge-resolution commit: a long single-line options object in
storage/git.ts and similar in hooks.test.ts. prettier --write
fixes both with the project's standard wrap-and-trailing-comma
style. No semantic change.

* test(git): include windowsHide in toHaveBeenCalledWith assertion

The merge-resolution commit added windowsHide:true to the
'git rev-parse --is-inside-work-tree' execSync call in
src/storage/git.ts, but the matching strict-shape assertion in
git.test.ts:31-34 still expected the pre-patch two-key options
object {cwd, stdio}. vitest's toHaveBeenCalledWith does a deep
structural match, so the extra third key flipped the assertion
to fail.

Add windowsHide: true to the expected shape. Only this one
assertion is strict; the two siblings ('passes the correct cwd'
and the no-cwd-arg case) use expect.objectContaining and
expect.any(String) and remain green without modification.

* test(setup-codex): include windowsHide in execFile shape assertions

Same root cause as the git.test.ts fix on this branch: the windowsHide
patch added windowsHide:true to the execFile() options in
src/cli/setup.ts, but three strict-shape toHaveBeenCalledWith
assertions in setup-codex.test.ts still expected the pre-patch
{shell:true} / {shell:false} two-key options. vitest does a deep
structural match, so the extra key flipped the assertions to fail
on every CI matrix leg (ubuntu coverage + macos + windows).

Adding windowsHide:true alongside the existing 'shell' key in
all three sites.

* ci: retrigger checks

go-parity failed on a flaky onnxruntime-node postinstall network timeout
(AggregateError [ETIMEDOUT] in node ./script/install), which cascaded into
the CI Gate. No code change — empty commit to re-run the pipeline.

* fix(test): strengthen windowsHide regression assertions (PR #1794 review)

- Replace toBeGreaterThanOrEqual with exact toBe per DoD §2.7
- Remove unused `m` variable in countSpawnCalls (CodeQL finding)
- Add windowsHide: true to runGit test helper for consistency

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: ManniX-ITA <35522085+ManniX-ITA@users.noreply.github.com>
Co-authored-by: Test <test@example.com>
2026-05-24 09:51:21 +01:00
Derek Pearson
89c03b2ebb
fix: skip Claude augment hook when GitNexus server owns DB (#1493)
* fix(claude): skip augment hook when server owns db

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

* fix(hooks): cross-platform DB lock probe for MCP owner guard

Extract hook-db-lock-probe.cjs with a single hasGitNexusDbLockedByGitNexusServer
entry point used by both Claude hooks:

- Linux: scan /proc/<pid>/fd via dev+inode (no lsof required), optional lsof
  fallback; GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS caps scan time
- macOS and other Unix: trusted lsof + ps (absolute paths / env overrides)
- Windows: Restart Manager + Win32_Process via win-rm-list-json.ps1 and
  GITNEXUS_HOOK_POWERSHELL_PATH

Update hooks.test.ts source coverage for the probe module.

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

* Update gitnexus/hooks/claude/win-rm-list-json.ps1

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Apply suggestion from @github-actions[bot]

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(gitnexus): repair package.json JSON after malformed engines edit

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

* Update Node.js engine version requirement to 22.0.0

* Update Node.js engine version to >=22.0.0

* fix(hooks): address ce-code-review findings on PR #1493

P0:
- Replace malformed `RM_UNIQUE_PROCESS` block in
  `gitnexus/hooks/claude/win-rm-list-json.ps1` (duplicate struct decl +
  duplicate `ProcessStartTime` + unbalanced braces) with a single
  well-formed `[StructLayout(LayoutKind.Sequential, Pack = 4)]` struct,
  so PowerShell `Add-Type` actually compiles and the Windows DB-lock
  probe stops fail-open on every machine.
- `gitnexus/src/cli/setup.ts` now copies `hook-db-lock-probe.cjs` and
  `win-rm-list-json.ps1` into the user's `~/.claude/hooks/gitnexus/`
  alongside `hook-lock.cjs`, preventing the `MODULE_NOT_FOUND` thrown
  by `gitnexus-hook.cjs:18`'s top-level require on every fresh install.
  `gitnexus/test/unit/setup.test.ts` extended to assert both new copy
  destinations.
- Four fail-open hook tests (`ENOENT lsof`, `npx parent line`,
  `non-GitNexus ps line`, `ps ENOENT`) now seed `createHookToolDir`
  with a valid `[GitNexus]` stderr line so
  `expect(parseHookOutput).not.toBeNull()` actually holds on CI.

P1:
- Plugin copy of `win-rm-list-json.ps1` gains `Pack = 4` so its CLR
  struct matches the 12-byte native `RM_UNIQUE_PROCESS` layout
  (multi-blocker `RmGetList` no longer reads mangled `dwProcessId`).
- `GITNEXUS_HOOK_CLI_PATH = ''` now falls through to the resolution
  chain in `gitnexus-hook.cjs`, matching the plugin copy and removing
  the twin-file divergence on empty-string envs.
- Lock-warning suppression test seeds `gitnexusMarkerPath` and asserts
  the augment subprocess actually ran, plus `GITNEXUS_DEBUG=1`
  preserves the full discarded prefix.
- MCP-owner skip branch in both hook copies now emits
  `[GitNexus] augment skipped: MCP server owns DB` on stderr, so
  agents can distinguish intentional skip from silent failure.

P2:
- `ps` loop in `hook-db-lock-probe.cjs` fails-closed on `ETIMEDOUT`
  to mirror the `lsof` handling (symmetric subprocess-probe contract).
- `RmStartSession` return value captured in both `.ps1` copies; exits
  early with `[]` on non-zero so subsequent RM API calls don't operate
  on an invalid handle.
- Windows RM-list `.ps1` encoded cache distinguishes uninitialized
  (`undefined`) from load-failed (`null`) with a one-shot
  `GITNEXUS_DEBUG` warning instead of silently caching empty string.
- `createHookToolDir` helper accepts `lsofOutputLines` and
  `psOutputByPid`; the multi-PID test uses them instead of duplicating
  the fake-binary construction inline.
- All five skip-path tests now assert `result.status === 0` and the
  new skip-signal stderr line.
- `AGENTS.md` documents the seven hook configuration env vars
  (`GITNEXUS_HOOK_CLI_PATH`, `_LSOF_PATH`, `_PS_PATH`,
  `_POWERSHELL_PATH`, `_LINUX_PROC_BUDGET_MS`, `_RM_TARGET`,
  `GITNEXUS_DEBUG`).
- `GITNEXUS_DEBUG` path in `gitnexus-hook.cjs`/`.js` writes the full
  discarded stderr prefix instead of a 180-char preview.
- Inline comment in `hook-db-lock-probe.cjs` explains the intentional
  Windows ETIMEDOUT fail-closed semantics.
- Removed the unnecessary `as WriteFileOptions` cast and orphaned
  `import type { WriteFileOptions }` in `hooks.test.ts`.

P3:
- `isGitNexusServerCommand` unexported from
  `hook-db-lock-probe.cjs` (kept as private helper).
- Env-path overrides (`GITNEXUS_HOOK_CLI_PATH`,
  `_POWERSHELL_PATH`, `_LSOF_PATH`, `_PS_PATH`) require
  `fs.existsSync` before being returned, so typos / stale config fall
  through to the standard resolution chain.

Misc:
- `gitnexus/package.json` engines.node back to `>=22.0.0` (matches
  origin/main and the original PR reviewer's earlier request).

Twin-tree parity / CI sync mechanism tracked separately at
abhigyanpatwari/GitNexus#1591.

Test plan: vitest run test/unit/hooks.test.ts → 113 passed,
18 Unix-only skipped; setup.test.ts → 14 passed.

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

* trigger

---------

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: Cursor <cursoragent@cursor.com>
2026-05-14 16:39:30 +01:00