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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ CodeGraph indexing belongs to **`whygraph scan`, not `whygraph init`** — `init

### Auto-rescan git hooks

`whygraph hooks install` (opt-in; `cli/commands/hooks.py`) wires `post-commit` / `post-merge` / `post-rewrite` to keep the DBs current as the developer works — no daemon. Each hook execs a shared helper (`.whygraph/hooks/whygraph-scan`, gitignored) that runs `whygraph scan --no-llm-descriptions --no-remote` (git history + `codegraph sync` only — fast, offline, no token; LLM descriptions stay on lazy backfill). The helper is **detached** (commits return instantly) and **single-flight + coalescing** (portable `mkdir` lock + a `pending` flag, since macOS has no `flock`), so rapid commits neither stack nor drop the latest `HEAD`. Installs are **sentinel-guarded** (`# >>> whygraph managed >>>`) and append to a foreign hook rather than clobber it. The `--no-remote` flag on `scan` exists for this path; `db/engine.py` sets `PRAGMA busy_timeout` so a background rescan and a manual scan don't collide.
`whygraph hooks install` (opt-in; `cli/commands/hooks.py`) wires `post-commit` / `post-merge` / `post-rewrite` to keep the DBs current as the developer works — no daemon. Each hook execs a shared helper (`.whygraph/hooks/whygraph-scan`, gitignored) that runs `whygraph scan --skip-analyze --no-remote` (git history + `codegraph sync` only — fast, offline, no token; LLM descriptions stay on lazy backfill). The helper is **detached** (commits return instantly) and **single-flight + coalescing** (portable `mkdir` lock + a `pending` flag, since macOS has no `flock`), so rapid commits neither stack nor drop the latest `HEAD`. Installs are **sentinel-guarded** (`# >>> whygraph managed >>>`) and append to a foreign hook rather than clobber it. The `--no-remote` flag on `scan` exists for this path; `db/engine.py` sets `PRAGMA busy_timeout` so a background rescan and a manual scan don't collide.

Deferred (net-new, not built yet): a project registry for cross-repo orchestration, a persistent/server mode, and per-branch CodeGraph/WhyGraph databases.

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ evidence WhyGraph serves.
For a fast, offline pass - no remote calls, no LLM - skip both phases:

```bash
whygraph scan --no-remote --no-llm-descriptions
whygraph scan --no-remote --skip-analyze
```

Descriptions backfill lazily later, so this is a fine way to get started quickly. See
Expand Down
11 changes: 4 additions & 7 deletions docs/guide/editors.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ inside the repo, so you can commit it and every teammate's editor picks it up.
| `vscode` (alias `copilot`) | VS Code / GitHub Copilot | `.vscode/mcp.json` |
| `codex` | OpenAI Codex | `.codex/config.toml` |

Run `whygraph init --list-agents` to print these paths for your own checkout.
Run `whygraph init --help` to see the supported agents.

The generated config launches `whygraph-mcp` by bare command name, so the same checked-in file works
for everyone who has WhyGraph installed - no absolute paths to scrub.
Expand All @@ -32,19 +32,16 @@ for everyone who has WhyGraph installed - no absolute paths to scrub.
leaves your existing files alone; pass `--force` to overwrite them.

```bash
whygraph init --agent claude --no-install-assets # MCP wiring only, skip the .claude/ copy
whygraph init --agent claude --force # overwrite existing .claude/ files
whygraph init --agent claude # wire MCP + copy the .claude/ assets
whygraph init --agent claude --force # overwrite existing .claude/ files
```

## Useful flags

| Flag | What it does |
|---|---|
| `--print` | Print the MCP snippet to stdout instead of writing any file. Good for pasting by hand. |
| `--list-agents` | List supported agents and their config paths, then exit. |
| `--install-assets / --no-install-assets` | Copy (or skip) the agent's bundled assets. Default: copy. No-op for agents with no asset tree. |
| `--skip-preflight` | Skip the host-tool diagnostics. For known-good scripted environments. |
| `--force` | Overwrite existing asset files in the destination directory. |
| `--yes` / `-y` | Accept all defaults without prompting (also implied off a TTY). |

## Verify

Expand Down
6 changes: 3 additions & 3 deletions docs/guide/scanning.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ without polluting area history.

| Flag | Default | What it does |
|---|---|---|
| `--no-llm-descriptions` | off | Skip the per-commit LLM phase. Git and GitHub crawlers still run; descriptions backfill lazily and on a later full scan. |
| `--skip-analyze` | off | Skip the per-commit LLM phase. Git and GitHub crawlers still run; descriptions backfill lazily and on a later full scan. |
| `--codegraph / --no-codegraph` | on | Refresh the CodeGraph index concurrently with the crawl. |
| `--codegraph-image TEXT` | pinned tag | Override the Docker image for the CodeGraph fallback. Ignored when a local `codegraph` binary is found. |
| `--remote / --no-remote` | on | Crawl the remote for PRs and issues per `[scan].provider`. `--no-remote` is a fast, offline, token-free scan. |
Expand All @@ -38,7 +38,7 @@ without polluting area history.
A common fast pass while iterating:

```bash
whygraph scan --no-remote --no-llm-descriptions
whygraph scan --no-remote --skip-analyze
```

!!! tip "Lazy backfill"
Expand All @@ -55,7 +55,7 @@ whygraph hooks install
```

This wires `post-commit`, `post-merge`, and `post-rewrite` to run
`whygraph scan --no-remote --no-llm-descriptions` **in the background**. Git history and a CodeGraph
`whygraph scan --no-remote --skip-analyze` **in the background**. Git history and a CodeGraph
`sync` only - no LLM, no remote calls - so commits stay instant and the scan is offline and
token-free.

Expand Down
12 changes: 5 additions & 7 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,12 @@ agents are project-scoped, so the config file is written inside the repo.

| Option | Description |
|---|---|
| `--agent [claude\|codex\|copilot\|cursor\|vscode]` | Wire the MCP server into the named agent's config. On a terminal, skips the interactive agent prompt. |
| `--agent [claude\|codex\|copilot\|cursor\|vscode]` | Wire the MCP server into the named agent's config. On a terminal, skips the interactive agent prompt. Run `whygraph init --help` for the full list of supported agents. |
| `--yes` / `-y` | Accept all defaults without prompting (also implied off a TTY). Writes a default `whygraph.toml` only if none exists. |
| `--print` | Print the MCP snippet to stdout instead of writing any config file. |
| `--list-agents` | List supported agents (with config-file paths) and exit. |
| `--install-assets / --no-install-assets` | Copy the chosen agent's bundled assets into the project. Default: enabled. No-op for agents that ship no asset tree. |
| `--skip-preflight` | Skip the host-tool diagnostics that normally run first. For known-good scripted environments. |
| `--force` | When installing assets, overwrite existing files in the agent's destination directory. |

Preflight diagnostics and asset install both always run — the chosen agent's bundled assets are copied into the repo automatically (use `--force` to overwrite local edits).

See [Wiring your editor](../guide/editors.md) for the per-agent paths.

## `whygraph scan`
Expand All @@ -59,7 +57,7 @@ picks up new commits and backfills what's missing.

| Option | Default | Description |
|---|---|---|
| `--no-llm-descriptions` | off | Skip the per-commit LLM description phase. The git and GitHub crawlers still run; descriptions backfill lazily on demand and on a later full scan. |
| `--skip-analyze` | off | Skip the per-commit LLM description phase. The git and GitHub crawlers still run; descriptions backfill lazily on demand and on a later full scan. |
| `--codegraph / --no-codegraph` | on | Refresh the CodeGraph index concurrently with the crawl - `codegraph sync` when an index exists, `codegraph init -i` on first run. A failure here warns rather than aborting. |
| `--codegraph-image TEXT` | pinned tag | Override the Docker image used for the CodeGraph refresh fallback. Ignored when a local `codegraph` binary is found. |
| `--remote / --no-remote` | on | Crawl the source-control remote (GitHub PRs / issues) per `[scan].provider`. `--no-remote` skips it for a fast, offline, token-free scan. |
Expand Down Expand Up @@ -99,5 +97,5 @@ whygraph hooks install
```

The hooks wire `post-commit`, `post-merge`, and `post-rewrite` to run
`whygraph scan --no-remote --no-llm-descriptions` in the background. See
`whygraph scan --no-remote --skip-analyze` in the background. See
[Keep it fresh](../guide/scanning.md#keep-it-fresh) for the details.
10 changes: 5 additions & 5 deletions src/whygraph/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class AgentTarget:
Serialization format of the target file. Determines which
renderer :func:`render_snippet` uses.
description : str
Short one-line description shown by ``whygraph init --list-agents``.
Short one-line description shown in ``whygraph init --help``.
assets_subdir : str or None
Name of the source directory under ``src/whygraph/assets/`` that
holds this agent's bundled asset tree, or ``None`` if the agent
Expand Down Expand Up @@ -268,8 +268,8 @@ def write_snippet(target: AgentTarget, project_root: Path) -> Path:
other servers and top-level keys are preserved.
* If the file exists but is unparseable, a fresh minimal config
replaces it. This is a conscious trade-off: we surface the new
config rather than refuse to proceed. Callers can offer
``--print`` for users who'd rather merge by hand.
config rather than refuse to proceed. Users who'd rather merge by
hand can render the snippet with ``render_snippet()`` instead.

Parameters
----------
Expand All @@ -293,8 +293,8 @@ def write_snippet(target: AgentTarget, project_root: Path) -> Path:
Comments and incidental formatting in the existing config file are
not preserved across the read-modify-write cycle (``tomllib`` strips
comments on parse; ``json.load`` collapses whitespace). Users who
care about preserving their hand-formatted config should use
``--print`` and merge the snippet manually.
care about preserving their hand-formatted config should render the
snippet with ``render_snippet()`` and merge it manually.
"""
if target.scope != "project":
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion src/whygraph/analyze/backfill.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The :class:`~whygraph.scan.analyze_crawler.AnalyzeCrawler` describes
every undescribed commit in bulk at scan time. When a scan is run with
``whygraph scan --no-llm-descriptions`` (or when a commit was added to
``whygraph scan --skip-analyze`` (or when a commit was added to
the database before its description could be generated), the
``commit.llm_description`` column is left ``NULL``. The MCP tools
:func:`whygraph.mcp.evidence.whygraph_evidence_for` and
Expand Down
2 changes: 1 addition & 1 deletion src/whygraph/cli/commands/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
if mkdir "$lock" 2>/dev/null; then
trap 'rmdir "$lock" 2>/dev/null' EXIT INT TERM
rm -f "$pending"
whygraph scan --no-llm-descriptions --no-remote >> "$log" 2>&1
whygraph scan --skip-analyze --no-remote >> "$log" 2>&1
rmdir "$lock" 2>/dev/null
trap - EXIT INT TERM
else
Expand Down
111 changes: 42 additions & 69 deletions src/whygraph/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,40 @@
from ..console import fail


@click.command(name="init")
def _agents_epilog() -> str:
"""Render the supported-agents block for ``whygraph init --help``.

Lists each agent's name, aliases, scope, format, and one-line
description — the discoverability the old ``--list-agents`` command
provided, minus its cwd-relative config path (help text isn't
cwd-anchored; the write step still echoes the real path). Built at
import from :data:`whygraph.agents.AGENTS`.

The leading ``\\b`` marker tells Click's help formatter not to
re-wrap the block, so the indented per-agent layout survives. Click
ends a "no-rewrap" paragraph at the first blank line, so the whole
block is kept as a single paragraph with no interior blank lines.
"""
lines = ["\b", "Supported agents (use with --agent):"]
for name in sorted(agents.AGENTS):
target = agents.AGENTS[name]
aliases = f" (aliases: {', '.join(target.aliases)})" if target.aliases else ""
scope = "project" if target.scope == "project" else "user"
lines.append(
f" {target.name}{aliases} — scope: {scope}, format: {target.format}"
)
lines.append(f" {target.description}")
return "\n".join(lines)


@click.command(name="init", epilog=_agents_epilog())
@click.option(
"--agent",
"agent_name",
type=click.Choice(agents.known_agent_names(), case_sensitive=False),
default=None,
help="Wire the WhyGraph MCP server into the named LLM agent's config.",
)
@click.option(
"--print",
"print_only",
is_flag=True,
help="Print the MCP snippet to stdout instead of writing any config file.",
)
@click.option(
"--list-agents",
"list_agents",
is_flag=True,
help="List supported agents (with config-file paths) and exit.",
)
@click.option(
"--install-assets/--no-install-assets",
"install_assets",
default=True,
help=(
"Copy the chosen agent's bundled assets (if any) into the project."
" Default: enabled. No-op for agents that ship no asset tree."
),
)
@click.option(
"--skip-preflight",
"skip_preflight",
is_flag=True,
help=(
"Skip the host-tool diagnostics that normally run at the top of"
" `whygraph init`. Use only in scripted environments where the"
" environment is known-good."
),
)
@click.option(
"--force",
"force",
Expand All @@ -75,10 +70,6 @@
)
def init_cmd(
agent_name: str | None,
print_only: bool,
list_agents: bool,
install_assets: bool,
skip_preflight: bool,
force: bool,
yes: bool,
) -> None:
Expand Down Expand Up @@ -106,27 +97,22 @@ def init_cmd(
subsequent run).

With ``--agent X``, registers the WhyGraph MCP server with the named
agent. All supported agents are project-scoped — their MCP config
file is written / merged in the repo. Pass ``--print`` to skip the
write and emit the snippet for manual pasting.
agent (run ``whygraph init --help`` for the list of supported
agents). All supported agents are project-scoped — their MCP config
file is written / merged in the repo. Agents whose config can't be
written automatically get the snippet printed for manual pasting.

If the chosen agent ships a bundled asset tree (see
:attr:`whygraph.agents.AgentTarget.has_assets`), the tree is copied
into the matching destination directory under the repo. Pre-existing
files are left alone unless ``--force`` is passed; pass
``--no-install-assets`` to skip the copy entirely.
files are left alone unless ``--force`` is passed.

Idempotent — re-running on an already-initialized project just
confirms both databases are present and at head.
"""
if list_agents:
_print_agent_list()
return

project_root = Path.cwd()

if not skip_preflight:
_run_preflight()
_run_preflight()

# Prompt only on a real terminal and only when not told to accept
# defaults. Pipes / CI / the git hooks have no TTY and fall straight
Expand All @@ -144,30 +130,30 @@ def init_cmd(
resolved_agent = answers.agent or agent_name
if resolved_agent is None:
click.echo(
"Tip: run `whygraph init --list-agents` to see supported agents,"
"Tip: run `whygraph init --help` to see supported agents,"
" then `whygraph init --agent <name>` to wire it up."
)
return

target = agents.resolve_agent(resolved_agent)
snippet = agents.render_snippet(target)

if print_only or not agents.is_write_supported(target):
if not agents.is_write_supported(target):
_print_snippet(target, project_root, snippet)
else:
path = agents.write_snippet(target, project_root)
click.echo(f"Wrote whygraph MCP entry to {path}")

if install_assets and target.has_assets:
if target.has_assets:
result = assets.install_assets(target, project_root, force=force)
_print_install_summary(target, project_root, result, force=force)


def _run_preflight() -> None:
"""Echo the diagnostics block; ``fail`` with a clean error on missing tools.

Imported here (not at module top) so ``--list-agents`` and ``--help``
don't pay the import cost.
Imported here (not at module top) so ``--help`` doesn't pay the
import cost.
"""
from whygraph.cli.preflight import PreflightError, run_preflight

Expand Down Expand Up @@ -224,8 +210,8 @@ def _scaffold_example_config(project_root: Path, answers) -> None:

Always refreshed so it tracks the shipped defaults and any non-secret
choices from ``answers``. Secrets are never written here. Lazy-imports
the config helper so lightweight surfaces like ``--list-agents`` and
``--help`` don't pay the cost.
the config helper so lightweight surfaces like ``--help`` don't pay
the cost.
"""
from whygraph.core.config import write_example_config

Expand Down Expand Up @@ -275,27 +261,14 @@ def _ensure_db_initialized() -> Path:
"""Bootstrap the WhyGraph DB, lazy-importing the heavy chain.

Imported here (not at module top) so that lightweight CLI surfaces
like ``--list-agents`` and ``--help`` don't fail when the DB layer
or its dependencies are mid-rewrite.
like ``--help`` don't fail when the DB layer or its dependencies are
mid-rewrite.
"""
from whygraph.db import ensure_initialized

return ensure_initialized()


def _print_agent_list() -> None:
click.echo("Supported agents:")
for name in sorted(agents.AGENTS):
target = agents.AGENTS[name]
aliases = f" (aliases: {', '.join(target.aliases)})" if target.aliases else ""
path = agents.config_path_for(target, Path.cwd())
scope = "project" if target.scope == "project" else "user"
click.echo(f" {target.name}{aliases}")
click.echo(f" scope: {scope} format: {target.format}")
click.echo(f" path: {path}")
click.echo(f" note: {target.description}")


def _print_snippet(
target: agents.AgentTarget, project_root: Path, snippet: str
) -> None:
Expand Down
12 changes: 6 additions & 6 deletions src/whygraph/cli/commands/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@

@click.command(name="scan")
@click.option(
"--no-llm-descriptions",
"no_llm_descriptions",
"--skip-analyze",
"skip_analyze",
is_flag=True,
default=False,
help=(
Expand Down Expand Up @@ -109,7 +109,7 @@
),
)
def scan_cmd(
no_llm_descriptions: bool,
skip_analyze: bool,
refresh_codegraph: bool,
codegraph_image: str | None,
remote: bool,
Expand All @@ -135,12 +135,12 @@ def scan_cmd(
else:
github_client = None

if no_llm_descriptions:
if skip_analyze:
# Bypass the LlmDescriptor probe entirely so a broken `[analyze]`
# config still lets users run a fast scan and rely on the MCP
# tools' lazy backfill for descriptions.
descriptor = None
analyze_skip: str | None = "--no-llm-descriptions"
analyze_skip: str | None = "--skip-analyze"
else:
try:
descriptor = LlmDescriptor.from_config(config.analyze)
Expand Down Expand Up @@ -334,7 +334,7 @@ def _optional_phase_cells(
"""Return the (status, summary, timing) cells for an optional phase.

A crawler that never ran (phase skipped via ``--no-remote`` /
``--no-llm-descriptions``) renders a dim ``— skipped`` status with no
``--skip-analyze``) renders a dim ``— skipped`` status with no
summary or timing.
"""
if crawler is None:
Expand Down
Loading
Loading