Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Checkout must stay LF everywhere: biome's format gate and the docs-as-code
# evals compare exact bytes, and the default autocrlf=true on Windows would
# turn every file CRLF and fail them.
* text=auto eol=lf
*.so binary
*.dylib binary
*.dll binary
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,32 @@ jobs:

- name: Hot-path bench
run: npx tsx src/cli.ts bench

test-windows:
name: test (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm

- name: Install dependencies
run: npm ci

- name: Type-check
run: npx tsc --noEmit

- name: Lint
run: npx biome check src test

- name: Unit and integration tests
run: npx vitest run

- name: Offline eval gates
run: npx tsx src/cli.ts eval all

- name: Hot-path bench
run: npx tsx src/cli.ts bench
4 changes: 3 additions & 1 deletion AGENT_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ You should see exactly 8 tools listed.
If multiple agents in the same repo will hit graphCTX at the same time
(Claude Desktop + Cursor + a CI worker), start one daemon and point all of
them at the same Unix socket. There is no TCP listener; the path is
filesystem-local only.
filesystem-local only. On Windows the same flag works: the path is mapped
into the named-pipe namespace (`\\.\pipe\...`) automatically, since Node
has no Unix-socket support there.

```bash
# start once per machine / repo
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ npx tsx src/cli.ts bench

Expected current state:

- `npx vitest run`: 292 tests pass.
- `npx vitest run`: 297 tests pass.
- `npm run pack:smoke`: packed tarball installs, runs the demo, and serves
MCP from a clean temp app.
- `npx tsx src/cli.ts eval all`: 19 gate suites pass.
Expand Down
6 changes: 3 additions & 3 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@
|---|---|---|
| `hook <event>` p95 | < 150ms | 15.51ms ✅ |

_Last updated: Private-key scanning now covers DSA, encrypted PEM, and PGP private key block headers, MCP `resolve_conflict` now evaluates user, workspace, and current-session facts together, `graphctx resolve` now refuses non-open-loop facts instead of silently superseding durable memory, Storage migration 0006 adds a composite invalidation conflict lookup index, Session-scoped invalidation now keeps identical open loops isolated across sessions while deduping repeats within a session, Demo seed memory now uses Runtime.rememberFact, open-loop session memory now carries current git anchors, CLI `tui --tab` now fails fast on invalid tabs, CLI/MCP fact-kind validation uses a shared `FACT_KINDS` domain enum, TUI promotion uses Runtime/Probation gates instead of direct activation, Runtime/TUI/CLI/MCP explicit remember paths share safe git-anchored invalidating writes, Runtime/TUI/MCP forget paths share git-anchored temporal closeout, MCP forget stamps git closeout anchors, retrieval candidates are scope-filtered before semantic reranking, retrieval candidates receive final workspace/session scope filtering, superseded facts close temporal validity with `t_expired` and commit anchors, retrieval recency ranking uses the runtime clock, ranking/conflict tests use deterministic fact IDs and clocks, vector embedding cache uses the runtime clock, outcome telemetry uses an injectable clock, injection anti-repetition ledger uses the runtime clock, promotion verification timestamps use the runtime clock, promotion gates rescan secret-shaped fact identity, why renders invalidating commit closeout anchors, revert revalidation respects branch representation, revert revalidation skips bad historical anchors, atomic temporal lifecycle closeout anchors, TUI read model redacts secret fact text, conflict notes redact identity/object secrets, structured why redacts evidence chain metadata, structured why redacts fact identity and scope strings, structured why redacts promotion audit text, structured why redacts git anchors, structured why redacts evidence payloads and tags, why output shows expired/invalidated temporal closeout, send-edge safety scans fact tags for secrets, command cards avoid false HEAD verification claims, low-trust claims blocked from PreToolUse guardrail injection, candidate facts kept out of vector search until promotion, lifecycle-synced vector index entries for expired/reactivated facts, secret-redacted semantic rerank text, retriever-level send-unsafe fact suppression, tag-update secret sensitivity restamping, secret-redacted FTS/vector fact indexing, FTS path-query fail-soft handling, runtime-level injection context redaction, hook tool-result redaction before injection context, shared retrieval-context redaction for CLI/MCP/hooks, atomic invalidation effects, supported-by temporal evidence edges, tool-arg redaction before retrieval, hook prompt redaction before retrieval, transcript-tail redaction before retrieval, symlink-safe workspace config reads, symlink-safe local store paths, symlink-safe AGENTS.md boot capsule writes, symlink-safe generic adapter marker writes, symlink-safe OpenCode config writes, symlink-safe Cursor mcp writes, symlink-safe Claude settings writes, workspace-confined CI workflow extraction, workspace-confined agent prose extraction, workspace-confined Docker/Compose extraction, workspace-confined test config extraction, workspace-confined tooling config extraction, workspace-confined tsconfig extraction, workspace-confined editorconfig extraction, workspace-confined runtime version extraction, workspace-confined lockfile extraction, workspace-confined package script evidence, shared realpath workspace evidence checks, symlink-aware injection staleness checks, generated-marker symlink skip, procedure verifier secret scanning, extraction subject secret scanning, hook session-id redaction, MCP session-reference secret refusal, open-loop session metadata secret refusal, explicit memory metadata secret refusal, packageManager canonical facts, package-manager-aware script extraction, runtime-version deterministic extraction, multi-cookie session redaction, MCP error redaction, fail-closed retrieval for unvalidated commit-scoped facts, semantic CLI recall ranking, auth/cookie header secret scanning, scoped semantic retrieval expansion, workspace-confined injection staleness checks, package metadata extraction, high-trust test/Docker/tooling/tsconfig deterministic extraction, high-trust Python toolchain extraction, high-trust pyproject package-manager extraction, pyproject dependency-group Python command extraction, high-entropy redaction hardening, repo-id-isolated temporal validity, measured hook latency, fail-closed adapter install/uninstall, typed CLI error formatting, answer-bearing coding query expansion, Python package-manager retrieval expansion, redacted TUI selected-fact detail panels, TUI page/jump keyboard navigation, composed TUI empty states with operator status surfaces, SessionStart suppression of superseded user preferences, chunk-safe TUI key decoding with left/right and j/k navigation, bounded secret-redacted TUI prompt input, vector-safe commit-hygiene query expansion, searchable TUI memory lists, and tox/nox Python test harness extraction. 292 tests, 19 gate suites green, all I1-I9 hold._
_Last updated: Windows is now a first-class dev platform (homedir-derived user-global paths, named-pipe daemon sockets, shim-free dev-tool spawns in eval suites, forward-slash generated-marker facts, windows-latest CI job), Private-key scanning now covers DSA, encrypted PEM, and PGP private key block headers, MCP `resolve_conflict` now evaluates user, workspace, and current-session facts together, `graphctx resolve` now refuses non-open-loop facts instead of silently superseding durable memory, Storage migration 0006 adds a composite invalidation conflict lookup index, Session-scoped invalidation now keeps identical open loops isolated across sessions while deduping repeats within a session, Demo seed memory now uses Runtime.rememberFact, open-loop session memory now carries current git anchors, CLI `tui --tab` now fails fast on invalid tabs, CLI/MCP fact-kind validation uses a shared `FACT_KINDS` domain enum, TUI promotion uses Runtime/Probation gates instead of direct activation, Runtime/TUI/CLI/MCP explicit remember paths share safe git-anchored invalidating writes, Runtime/TUI/MCP forget paths share git-anchored temporal closeout, MCP forget stamps git closeout anchors, retrieval candidates are scope-filtered before semantic reranking, retrieval candidates receive final workspace/session scope filtering, superseded facts close temporal validity with `t_expired` and commit anchors, retrieval recency ranking uses the runtime clock, ranking/conflict tests use deterministic fact IDs and clocks, vector embedding cache uses the runtime clock, outcome telemetry uses an injectable clock, injection anti-repetition ledger uses the runtime clock, promotion verification timestamps use the runtime clock, promotion gates rescan secret-shaped fact identity, why renders invalidating commit closeout anchors, revert revalidation respects branch representation, revert revalidation skips bad historical anchors, atomic temporal lifecycle closeout anchors, TUI read model redacts secret fact text, conflict notes redact identity/object secrets, structured why redacts evidence chain metadata, structured why redacts fact identity and scope strings, structured why redacts promotion audit text, structured why redacts git anchors, structured why redacts evidence payloads and tags, why output shows expired/invalidated temporal closeout, send-edge safety scans fact tags for secrets, command cards avoid false HEAD verification claims, low-trust claims blocked from PreToolUse guardrail injection, candidate facts kept out of vector search until promotion, lifecycle-synced vector index entries for expired/reactivated facts, secret-redacted semantic rerank text, retriever-level send-unsafe fact suppression, tag-update secret sensitivity restamping, secret-redacted FTS/vector fact indexing, FTS path-query fail-soft handling, runtime-level injection context redaction, hook tool-result redaction before injection context, shared retrieval-context redaction for CLI/MCP/hooks, atomic invalidation effects, supported-by temporal evidence edges, tool-arg redaction before retrieval, hook prompt redaction before retrieval, transcript-tail redaction before retrieval, symlink-safe workspace config reads, symlink-safe local store paths, symlink-safe AGENTS.md boot capsule writes, symlink-safe generic adapter marker writes, symlink-safe OpenCode config writes, symlink-safe Cursor mcp writes, symlink-safe Claude settings writes, workspace-confined CI workflow extraction, workspace-confined agent prose extraction, workspace-confined Docker/Compose extraction, workspace-confined test config extraction, workspace-confined tooling config extraction, workspace-confined tsconfig extraction, workspace-confined editorconfig extraction, workspace-confined runtime version extraction, workspace-confined lockfile extraction, workspace-confined package script evidence, shared realpath workspace evidence checks, symlink-aware injection staleness checks, generated-marker symlink skip, procedure verifier secret scanning, extraction subject secret scanning, hook session-id redaction, MCP session-reference secret refusal, open-loop session metadata secret refusal, explicit memory metadata secret refusal, packageManager canonical facts, package-manager-aware script extraction, runtime-version deterministic extraction, multi-cookie session redaction, MCP error redaction, fail-closed retrieval for unvalidated commit-scoped facts, semantic CLI recall ranking, auth/cookie header secret scanning, scoped semantic retrieval expansion, workspace-confined injection staleness checks, package metadata extraction, high-trust test/Docker/tooling/tsconfig deterministic extraction, high-trust Python toolchain extraction, high-trust pyproject package-manager extraction, pyproject dependency-group Python command extraction, high-entropy redaction hardening, repo-id-isolated temporal validity, measured hook latency, fail-closed adapter install/uninstall, typed CLI error formatting, answer-bearing coding query expansion, Python package-manager retrieval expansion, redacted TUI selected-fact detail panels, TUI page/jump keyboard navigation, composed TUI empty states with operator status surfaces, SessionStart suppression of superseded user preferences, chunk-safe TUI key decoding with left/right and j/k navigation, bounded secret-redacted TUI prompt input, vector-safe commit-hygiene query expansion, searchable TUI memory lists, and tox/nox Python test harness extraction. 297 tests, 19 gate suites green, all I1-I9 hold._

_Quality counters: Tests: 292. Gate suites: 19._
_Quality counters: Tests: 297. Gate suites: 19._

_Runtime-clock note: direct CLI, eval, and benchmark retrievers pass
`Runtime.clock`, so pull and push retrieval use the same recency clock seam._
Expand Down Expand Up @@ -119,4 +119,4 @@ now have explicit scanners instead of relying only on entropy fallback._
| Code quality | ✅ | new `eval quality` passes 6/6: full-repo Biome, strict TS config/scripts, shared command-surface helpers for CLI help/docs/README reachability, eval-suite runner/test coverage, final README docs-as-code, and generated migration packaging guard |

_Loop note: composite metric = (failing_gates × 100) + (un-perfected aspects); within-aspect
measured gains are recorded in the `memory` graph. Tests: 292, gate suites: 19 (`eval all` includes run/memory/promote/drift/retrieval/gate/security/branch/temporal/conflict/procedure/mcp/storage/telemetry/provenance/resilience/benchmarks/cli-docs-demo/quality)._
measured gains are recorded in the `memory` graph. Tests: 297, gate suites: 19 (`eval all` includes run/memory/promote/drift/retrieval/gate/security/branch/temporal/conflict/procedure/mcp/storage/telemetry/provenance/resilience/benchmarks/cli-docs-demo/quality)._
13 changes: 4 additions & 9 deletions src/adapters/claude-code/install.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { AdapterError } from "../../core/errors.js";
import type { InstallOptions } from "../adapter.js";
Expand All @@ -25,9 +26,7 @@ interface ClaudeSettings {
// event payload on stdin. Idempotent: replaces any prior graphctx entries.
export function installClaudeHooks(opts: InstallOptions): { settingsPath: string } {
const bin = opts.binPath ?? "graphctx";
const dir = opts.global
? join(process.env.HOME ?? "", ".claude")
: join(opts.workspaceDir, ".claude");
const dir = opts.global ? join(homedir(), ".claude") : join(opts.workspaceDir, ".claude");
mkdirSync(dir, { recursive: true });
const settingsPath = join(dir, "settings.json");
assertWritableConfigPath(settingsPath, "Claude settings.json file");
Expand Down Expand Up @@ -58,9 +57,7 @@ export function installClaudeHooks(opts: InstallOptions): { settingsPath: string
}

export function uninstallClaudeHooks(opts: InstallOptions): void {
const dir = opts.global
? join(process.env.HOME ?? "", ".claude")
: join(opts.workspaceDir, ".claude");
const dir = opts.global ? join(homedir(), ".claude") : join(opts.workspaceDir, ".claude");
const settingsPath = join(dir, "settings.json");
if (!existsSync(settingsPath)) return;
assertWritableConfigPath(settingsPath, "Claude settings.json file");
Expand All @@ -84,9 +81,7 @@ export function uninstallClaudeHooks(opts: InstallOptions): void {
}

export function hasClaudeGraphctxHooks(opts: InstallOptions): boolean {
const dir = opts.global
? join(process.env.HOME ?? "", ".claude")
: join(opts.workspaceDir, ".claude");
const dir = opts.global ? join(homedir(), ".claude") : join(opts.workspaceDir, ".claude");
const settingsPath = join(dir, "settings.json");
if (!existsSync(settingsPath)) return false;
if (isSymlink(settingsPath)) return false;
Expand Down
7 changes: 2 additions & 5 deletions src/adapters/skill/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { ValidationError } from "../../core/errors.js";
import { assertWritableConfigPath } from "../config-path.js";
Expand Down Expand Up @@ -31,11 +32,7 @@ export function isSkillClient(value: string): value is SkillClient {
return (SKILL_CLIENTS as readonly string[]).includes(value);
}

export function skillPath(
client: SkillClient,
workspaceDir: string,
homeDir = process.env.HOME ?? "",
): string {
export function skillPath(client: SkillClient, workspaceDir: string, homeDir = homedir()): string {
switch (client) {
case "claude":
return join(workspaceDir, ".claude", "skills", "graphctx", "SKILL.md");
Expand Down
6 changes: 3 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { homedir, tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { Command } from "commander";
import { writeAgentsCapsule } from "./adapters/boot-capsule.js";
Expand Down Expand Up @@ -281,7 +281,7 @@ program
.option("--mcp", "run as MCP server (stdio JSON-RPC)")
.option(
"--socket <path>",
"(daemon mode) bind to a local Unix socket; one server, many concurrent clients",
"(daemon mode) bind to a local Unix socket (win32: named pipe); one server, many clients",
)
.option("-C, --cwd <dir>", "workspace directory", process.cwd())
.description("run the graphCTX MCP server (stdio by default, 8 tools)")
Expand Down Expand Up @@ -915,7 +915,7 @@ function handleCliError(e: unknown): never {

function logError(e: unknown): void {
try {
const dir = join(process.env.HOME ?? "", ".local", "share", "graphctx", "logs");
const dir = join(homedir(), ".local", "share", "graphctx", "logs");
mkdirSync(dir, { recursive: true });
const line = `${isoNow()} ${e instanceof GraphCtxError ? `[${e.code}] ` : ""}${(e as Error)?.message ?? String(e)}\n`;
writeFileSync(join(dir, "hook-errors.log"), line, { flag: "a" });
Expand Down
3 changes: 2 additions & 1 deletion src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join, resolve } from "node:path";
import { ConfigError } from "../core/errors.js";
import { symlinkedPathComponent } from "../core/path-safety.js";
Expand Down Expand Up @@ -93,7 +94,7 @@ function readWorkspaceJsonIfSafe(path: string, workspaceDir: string): unknown {

function expandPath(p: string, workspaceDir: string): string {
let out = p;
if (out.startsWith("~")) out = join(process.env.HOME ?? "", out.slice(1));
if (out.startsWith("~")) out = join(homedir(), out.slice(1));
if (out.includes("~/.local/share/graphctx")) {
out = out.replace(/^.*\.local\/share\/graphctx/, dataDir());
}
Expand Down
20 changes: 20 additions & 0 deletions src/eval/dev-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { join } from "node:path";

// JS entry points for the dev tools the eval suites and tests spawn.
// Spawned via process.execPath instead of the node_modules/.bin shims: on
// win32 those shims are .cmd batch files, which execFile refuses to spawn
// without shell:true (CVE-2024-27980 hardening), failing with EINVAL.
// Running the current node binary against each tool's real entry is
// shim-free, injection-safe, and identical on every platform.

export function tsxEntry(repoRoot: string): string {
return join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
}

export function vitestEntry(repoRoot: string): string {
return join(repoRoot, "node_modules", "vitest", "vitest.mjs");
}

export function biomeEntry(repoRoot: string): string {
return join(repoRoot, "node_modules", "@biomejs", "biome", "bin", "biome");
}
Loading
Loading