Commit Graph

308 Commits

Author SHA1 Message Date
Safi 37d7078f8b Prototypes: repo-scoped import-first resolution + cross-language API-contract edges
Flag-gated experiments (GRAPHIFY_REPO_SCOPED_RESOLUTION, GRAPHIFY_API_CONTRACT_EDGES),
validated but intentionally NOT on v8. Parked here so the work is recoverable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:43:50 +01:00
Safi 8e6ba9d714 Parse package manifests into canonical package nodes + depends_on edges (#1377)
apm.yml was a .yml document handled by the LLM, so the same package got a
different file-anchored node id from its own manifest than from each dependent's
dependency reference and split into duplicate nodes. New manifest_ingest module
parses apm.yml/pyproject.toml/go.mod/pom.xml deterministically into ONE package
node per package, keyed by name via ids.make_id, plus depends_on edges; routed
to the AST path (CODE) so the LLM never sees them. Package nodes are exempt from
the file-stem prefix remap so the canonical id is stable across manifests and
dedup collapses references to a single hub node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:25:09 +01:00
Safi a78956424d Reject Windows-style git hooks paths instead of creating a junk dir (#1385)
On WSL/POSIX, Path("C:\\...").is_absolute() is False, so a Windows absolute
core.hooksPath (or rev-parse --git-path output) was joined under the repo root
and mkdir'd as a literal backslash-named junk directory while install reported
success and the real .git/hooks got nothing. Both branches of _hooks_dir now
reject drive-letter / backslash paths with a clear error so the failure is loud.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:06:07 +01:00
Safi e6eaad3d7e Emit edges for markdown links so hub docs connect (#1376)
extract_markdown only emitted heading nodes + contains edges and never parsed
link syntax, so a doc full of [text](./other.md) links (index.md,
table-of-contents.md) had no edges to the docs it links and never became a hub.
Add a deterministic link pass: inline, reference-style, and [[wikilinks]],
resolved relative to the source file, external URLs/anchors/images skipped, with
the target id built via the same _make_id recipe so the edge merges onto the
real doc node instead of an orphan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:02:27 +01:00
Safi 5038129c7e Accept str paths in the semantic extract entry points (#1386)
extract_corpus_parallel and extract_files_direct are typed list[Path] but
crashed with AttributeError on str paths (f.suffix in slicing/partition, f.parent
in packing). Coerce files = [Path(f) for f in files] at both public entry points;
the AST extract() already coerced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:01:31 +01:00
Safi 8883991c2e Unify node-ID normalization into a single source of truth (#1378)
extract._make_id and build._normalize_id were copy-pasted forks of the same
NFKC/casefold recipe kept in sync by mirrored docstrings -- the root of the
recurring ID-drift ghost-node bug class (#811/#550/#1033/#1104). Move the recipe
to graphify/ids.py and have all four producers delegate to it: extract, build,
and (completing the migration) mcp_ingest and symbol_resolution, whose
"avoid an import cycle" copies are moot now that ids.py is dependency-free. The
contract test asserts all four resolve to the shared recipe, with hypothesis
property tests for make_id == normalize_id and idempotency.

Co-Authored-By: danielnguyenfinhub <danielnguyenfinhub@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:55:44 +01:00
Safi ab1e0ec588 Adaptive split-and-retry for community labeling on parse failure (#1280, #1278)
label_communities logged-and-skipped any batch whose LLM response was malformed
JSON, silently losing ~100 names per failed batch on large graphs. Split the
batch at the midpoint and retry each half (smaller prompt -> smaller output),
mirroring _extract_with_adaptive_retry; the base case re-raises so the caller
skips just that batch. Removed leftover scaffolding and corrected the docstring.

Co-Authored-By: CJdev232 <CJdev232@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:50:52 +01:00
Safi 4f539e729f Slice oversized text documents so the whole file is extracted (#1369)
_read_files capped every file at 20,000 chars, so a Markdown/text/rST document
longer than that lost everything past the cap with no recovery. Oversized
splittable-text files are now split at heading/paragraph boundaries into units
that each fit the cap and together cover the whole file; every slice reports its
parent file as source_file so the graph isn't fragmented per-slice, and a slice
that still overflows output is bisected and retried. Code and PDFs are never
sliced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:55:56 +01:00
Safi 897483fc96 Merge #1366: prune deleted-only and root the full build so --update stops duplicating
After #1361 added root= to the --update build_merge call, build_merge's prune
(which runs after the merge) began matching freshly re-extracted changed-file
nodes and deleting them — a regression in 0.8.41 where --update on a changed
file wiped its nodes. This drops `changed` from prune_sources (replace-on-
re-extract from #1344 already reconciles changed files), passes root= to the
full build's build_from_json so its node-key base matches the update side, and
pins the extraction-spec source_file to the verbatim path so the two runs never
drift. Re-blessed skillgen expected/ snapshots.

Co-Authored-By: RelywOo <RelywOo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:28:19 +01:00
Safi 895a60f4e3 Model Java records as type nodes and extract constructor calls (#1373)
Add record_declaration to the Java class types so a record becomes a first-class
type node instead of an isolated file node, and add object_creation_expression
to call types with a dedicated branch that reads the callee from the `type`
field (new Foo() has no `name` field), so `new Foo(...)` emits a calls edge to
the constructed type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:18:48 +01:00
Safi b2a1722903 Merge .graphifyignore with .gitignore instead of replacing it (#1363)
A .graphifyignore made graphify skip that directory's .gitignore entirely, so a
file excluded only by .gitignore (including neutrally-named secrets the
sensitive-file heuristic misses) got indexed into the graph and could leak into
committed graph artifacts. Read .gitignore first and .graphifyignore last so
their patterns merge and graphifyignore negations still win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 01:15:24 +01:00
Safi 5b0c154828 Honour configured output-token cap for OpenAI-compatible backends (#1365)
ollama/openai/deepseek/kimi set max_tokens in their backend config, but the
openai-compat dispatch read only max_completion_tokens (which only gemini
defines), so their output silently capped at the 8192 fallback and truncated
deep-mode JSON. Read either key and give the openai config an explicit cap;
GRAPHIFY_MAX_OUTPUT_TOKENS still overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:35:58 +01:00
Safi e9f8dde7af Stop fuzzy dedup over-merging numbered siblings, cross-file boilerplate, and prefix-divergent labels (#1284, #1243)
Three pass-2 guards (mirrored in the --dedup-llm pair collection): block merges
when labels' embedded numbers differ as zero-padding-insensitive multisets;
block cross-file merges of file-anchored rationale/document nodes (same-file
still merges); and score cross-file long labels on plain Jaro instead of
Jaro-Winkler so the prefix bonus can't fabricate merges of shared-prefix but
token-divergent entities (jest-native vs react-native), while genuine cross-file
duplicates still clear Jaro and same-file near-duplicates keep Jaro-Winkler.

Co-Authored-By: van4oza <van4oza@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:21:19 +01:00
RelywOo 5b988974ca fix(skill): pin extraction source_file + root the full build, prune deleted-only
Completes the source_file convention fix begun in #1344 (build_merge
replace-on-re-extract) and #1361 (pass root= to build_merge in the --update
runbook). Two gaps still let the full build and incremental --update emit
different source_file bases for the same file, so the source_file-keyed replace
missed and duplicates accumulated:

1. extraction-spec(.md/-compact.md): the subagent's source_file slot was an
   unpinned "relative/path", so it invented a base per run (and the node id,
   derived from the same path, drifted too). Pin it to the verbatim FILE_LIST
   path so _norm_source_file(root) canonicalizes every run identically.

2. core.md: the full build called build_from_json WITHOUT root=, so #1361's
   update-side root= had no matching base on the full-build side. Pass
   root='INPUT_PATH' at both sites (Step 4 export, Step 5 report) so the full
   build and --update relativize to the same base.

update.md prune_sources = deleted only. Changed files are replaced by build_merge
(#1344); once root= aligns the bases, leaving `changed` in prune_sources would
delete the freshly re-extracted nodes.

Engine (build.py) unchanged. Regenerated all skill artifacts via
tools/skillgen/gen.py. Adds test_build_merge_root_collapses_convention_drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:07:14 +03:00
Safi 8437ef465e Backfill edge source_file from endpoint nodes (#1279)
Semantic/LLM edges occasionally omit source_file, which build only normalized
when already present, so the field reached graph.json empty and downstream
validation flagged it. Backfill from the source (then target) node in
build_from_json and in the --no-cluster raw-write path, which bypasses it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:49:31 +01:00
Safi 9c8bd4d47c Populate Obsidian canvas when no community data is present (#1324)
to_canvas built cards solely by iterating communities, so a graph with no
community data (--no-cluster builds, or a missing analysis sidecar) wrote the
empty 32-byte {"nodes":[],"edges":[]} shell while notes rendered fine. Fall back
to one synthetic community covering every node so the canvas reflects the graph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:49:31 +01:00
Safi 2e01f37c81 Resolve cross-file Swift class relationships from member calls and constructors (#1356)
Capture property/field initializer constructor calls, build a per-file Swift
type table from property/parameter declarations, and add a member-call
resolution pass that types the receiver and emits an edge only when the type
name resolves to exactly one definition. Additive and INFERRED-only; the
is_member_call drop and the #543/#1219 god-node guards stay intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:08:59 +01:00
RelywOo fd463deb03 fix(build_merge): replace re-extracted files instead of accumulating stale edges (#1344)
build_merge: prune a re-extracted file's stale nodes/edges before merge instead of accumulating (fixes #1283, #1285). Validated: full suite 2107 passed. Thanks @RelywOo.
2026-06-17 10:46:15 +01:00
geektan123 f117aaccc6 feat: PowerShell .psd1 manifest parsing & Import-Module / dot-source edge emission (#1331) (#1341)
Index PowerShell .psd1 manifests + emit Import-Module/dot-source edges (closes #1331). Builds on the shipped .psm1 support. Validated: full suite 2107 passed, 18 new tests. Thanks @geektan123.
2026-06-17 10:46:12 +01:00
Pavel Kudinov a16b3e1b45 fix incremental no-cluster graph updates (#1350)
Harden incremental no-cluster updates: fixes empty-write graph wipeout on no-op update --no-cluster (#1347) and git-hook subdir path resolution (#1348). Complementary to #1317. Validated: full suite 2107 passed, no-op re-run no longer wipes graph. Thanks @pkudinov.
2026-06-17 10:46:08 +01:00
balloon72 9a7dbfbb84 fix js workspace import resolution (#1352)
Fixes #1334: detect npm/yarn package.json workspaces (array + yarn object form), pnpm precedence preserved. Reviewed: full suite 2087 passed, no regressions. Thanks @balloon72.
2026-06-17 10:29:47 +01:00
balloon72 d885833112 fix node label lookup normalization (#1353)
Fixes #1338 (Unicode NFD/NFC): serve._find_node now matches tokenized labels; affected.resolve_seed NFC-normalizes + casefolds. Reviewed: full suite 2087 passed, CLI smoke clean, no regressions. Thanks @balloon72.
2026-06-17 10:29:43 +01:00
Safi be3dcfca08 Unify query skill: ship expansion + inline fallback to every platform
The query skill was split across two fragments so no platform got both
capabilities: Claude had the vocab/IDF query-expansion step but no fallback if
the CLI was unavailable; every other platform had the inline NetworkX fallback
but the weaker raw-question matcher. Merge into one unified query reference +
stub (Step 0 expansion -> CLI traversal -> inline NetworkX fallback, plus
path/explain inline) shipped to all hosts. Remove the query_variant enum, its
toml field, and the _CLI_ONLY_QUERY_HEADINGS coverage-audit exemption. Re-render
all skill artifacts and re-bless expected/. skillgen check/audit-coverage/
monolith-roundtrip/schema-singleton all pass. Refs #1325.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 03:33:23 +01:00
Safi 2100b3d05b Resolve cross-file Java type references by import package
Same-named types in different packages left implements/inherits edges stuck on
bare shadow stubs, isolating the real interface (_rewire_unique_stub_nodes only
fixes the globally-unique case). New _resolve_java_type_references pass uses each
referencing file's import statements (+ package decl) to build an FQN->def index,
re-points dangling implements/inherits/imports edges to the exact definition, and
drops the orphaned stub. External/stdlib imports stay unresolved (correct). Runs
after id-disambiguation so target ids are final. Java-scoped; other _extract_generic
languages share the same bare-name fallback and remain a follow-up.

Adds tests/test_java_type_resolution.py (simple, ambiguous-by-import, build-survival).
Refs #1318.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 03:16:08 +01:00
Safi 4fbb33301c Collapse Swift module imports to one shared node
Adopts the approach from #1330 (thanks @duncan-daydream) on top of the v0.8.40
Swift import fix: _import_swift returns (id,label) module pairs, the extractor
materializes a type=module anchor node per import, and _disambiguate_colliding_node_ids
exempts type=module nodes so the same module imported from N files collapses to
one shared node (enables reverse traversal "what imports CoreKit"). The --no-cluster
writer now dedupes nodes by id and edges to match the clustered build_from_json path.

Replaces the interim _import_label/synthesize_import_module_nodes mechanism.
Adds tests/test_swift_import_resolution.py (cross-file collapse, build survival)
and dedupe_nodes coverage. Refs #1327, #1330.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 03:06:43 +01:00
Safi adb52a1ea0 Index .psm1, anchor Swift import targets, dedupe no-cluster edges
- #1315: add .psm1 to CODE_EXTENSIONS + _DISPATCH so PowerShell modules are indexed
- #1327: synthesize a module node for Swift import targets (new LanguageConfig
  flag synthesize_import_module_nodes) so imports edges survive build.py pruning;
  strengthen the Swift dangling-edge test to also assert edge targets
- #1317: dedupe parallel edges by (source,target,relation) in the --no-cluster
  and incremental update write paths so edge counts are deterministic and
  `update` is idempotent

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 02:28:16 +01:00
Siddarth Chintamani 09da5294e4 feat: extract JS/TS this.X=, exports.X=, prototype, class arrow fields, function expressions (#1323)
Extract JS/TS this.X=, exports.X=, prototype, class arrow fields, and function expressions (closes #1322). Validated locally against v8: full suite 2069 passed.
2026-06-16 02:14:31 +01:00
Timon 61836ce0e8 feat: custom endpoints via OPENAI_BASE_URL/OPENAI_MODEL and ANTHROPIC_BASE_URL/ANTHROPIC_MODEL (#1273)
Custom OPENAI/ANTHROPIC base-url + model env vars for self-hosted and proxy endpoints. CI green (3.10/3.12).
2026-06-16 01:41:15 +01:00
Safi b83086297f fix: four production bugs — Windows crashes, ghost-merge collision, version probe
extract.py: clamp ProcessPoolExecutor max_workers to 61 on Windows (issue #1298).
Python's ProcessPoolExecutor hard-caps at 61 on Windows via WaitForMultipleObjects;
>61-core machines crashed on AST extraction. Clamp applied after all input paths
(auto-compute, GRAPHIFY_MAX_WORKERS, --max-workers) to cover all three.

build.py: skip ghost-merge when two AST nodes share (basename, label) key (issue #1257).
When same-named symbols appear in same-named files across directories (e.g. two
render() in two index.ts), last-writer-wins produced an arbitrary canonical node
and mis-pointed all edges. Now tracked in _loc_collisions; ambiguous keys are
skipped in Pass 2, leaving the ghost intact rather than merging into the wrong node.

__main__.py: ignore OSError on unreadable .graphify_version probes (issue #1299).
On restricted-permission installs or network mounts, .exists()/.read_text() raised
PermissionError and crashed every graphify query/explain/path call at startup.
All three FS probes now wrapped in try/except OSError: return.

prs.py: resolve claude.cmd on Windows in prs.py claude-cli backend (issue #1288).
The _call_llm and _call_claude_cli paths were already fixed; prs.py had the same
bare ["claude", ...] call that fails on Windows npm installs with WinError 2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 15:13:23 +01:00
Safi 70a1ad7278 Merge pull request #1176 from galshubeli/falkordb-backend
feat: add FalkorDB export backend (#1175)
2026-06-12 20:15:30 +01:00
Safi fb1908cc55 Merge pull request #1282 from balloon72/codex/community-label-model
feat: add --model to community labeling commands
2026-06-12 19:54:44 +01:00
Safi 90decbc4ed Merge pull request #1276 from papinto/fix/graphifyignore-negation-pruning
fix: a single negation (!) in .graphifyignore no longer disables all directory pruning
2026-06-12 19:54:41 +01:00
hanmo1 b304331d24 feat: add model override for community labeling 2026-06-12 17:57:19 +08:00
s22hyun 2ab2302112 fix(affected): handle "edges"-keyed graph.json in load_graph (KeyError: 'links') 2026-06-12 15:33:33 +09:00
Jamie Evans 6dc23db90f fix: emit symbol edges for default imports/exports
JS/TS symbol resolution only handled named imports. A default-exported
symbol (`export default class Foo`) imported as `import Foo from './foo'`
produced only a file→file `imports_from` edge; the class node received no
incoming symbol edge. On codebases that default-export most classes
(NestJS services/helpers/models, etc.) those symbols looked like isolated
leaf nodes, and `graphify affected "<Class>"` / `explain` reported no
callers.

Record default imports with imported_name="default", register a "default"
export for `export default <class|function|identifier>`, and let the
existing exported-origin resolver wire the `imports` edge — which also
resolves calls made through the local binding, even when it is renamed
(`import Bar from './foo'; new Bar()`). `export { X as default }` was
already handled via the export-clause path; anonymous defaults
(`export default class {}`) have no resolvable symbol and stay file-level.

Adds regression tests for default-export class/function/identifier import
resolution and renamed-binding call resolution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:24:59 +01:00
Safi d276a5aa57 Merge pull request #1271 from giovanecesar/feat/cargo-introspect
feat(extract): add opt-in --cargo crate dependency extractor
2026-06-11 23:22:50 +01:00
Safi 60d4b4373f Merge pull request #1269 from jevans-agent/fix/tsconfig-paths-baseurl
fix: resolve tsconfig path aliases relative to baseUrl
2026-06-11 23:22:45 +01:00
Safi e16c1f37bf Merge pull request #1266 from PowPickles/fix/claude-cli-windows-spawn
fix: claude-cli backend works headlessly on Windows npm installs
2026-06-11 23:22:41 +01:00
Safi b87329496f Merge pull request #1265 from PowPickles/fix/resolve-seed-callable-decoration
fix: resolve_seed matches bare names against ()-decorated callable labels
2026-06-11 23:22:38 +01:00
Safi 0be4e81047 Merge pull request #1246 from VLDCNDN/fix/extract-out-cache-leak
fix: anchor extraction cache at --out root so external output leaves the scanned project clean
2026-06-11 23:22:35 +01:00
Safi 5b60624acd Merge pull request #1267 from TheFedaikin/v8
uv.lock sync, SystemVerilog class-level extraction + Dart mixin fix
2026-06-11 23:22:27 +01:00
Safi 3ea6def727 Merge pull request #1262 from teemow/fix/collect-files-single-walk
fix(extract): collect files in a single pruned walk instead of one rglob per extension (#1261)
2026-06-11 23:22:23 +01:00
Safi b34902b8d7 Merge pull request #1260 from teemow/fix/frontmatter-delimiters
fix(cache): require whole --- lines as frontmatter delimiters (#1259)
2026-06-11 23:22:20 +01:00
Safi e364f72fd8 Merge pull request #1253 from teemow/fix/ast-cache-version
fix(cache): namespace AST cache by graphify version (#1252)
2026-06-11 23:22:15 +01:00
Safi 556cab3c2c Merge pull request #1251 from teemow/fix/global-graph-external-edges
fix(global-graph): rewire edges to deduplicated external nodes (#1250)
2026-06-11 23:22:11 +01:00
Paulo Pinto e8ab8f4172 fix: negation pattern no longer disables all directory pruning
A single `!` rule in .graphifyignore set a blanket `has_negation` flag that
disabled directory-level pruning for EVERY ignored directory during the
os.walk in detect(). One unrelated `!docs/**` therefore made the walk descend
bin/, obj/, wwwroot/, generated/, … on large repos — a pathological slowdown.
Output stayed correct (the per-file `_is_ignored` filter still excluded those
files), but the walk visited the entire tree.

The bypass was unnecessary: `_is_ignored` already honours negations correctly —
last-match-wins lets `!dir/` un-ignore a directory (so it is not pruned), and
the gitignore parent-exclusion rule means a `!` cannot rescue a file beneath an
excluded directory, so descending an ignored dir to find a re-included file is
never needed. Prune purely on `_is_noise_dir` + `_is_ignored`.

Adds a regression test that tracks os.walk and asserts the ignored dir is never
descended while the negation still re-includes its target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:50:44 -07:00
Giovane Cesar da Silva b56a415388 feat(extract): add opt-in --cargo crate dependency extractor
Closes #1264

Also fixes a narrow watch-mode path-filtering bug found while running the required local
full-suite gate for this PR. Hidden directories inside the watched corpus are still ignored;
hidden ancestors outside the watched root no longer suppress valid file events.
2026-06-11 15:08:36 -06:00
Jamie Evans ec04152a90 fix: resolve tsconfig path aliases relative to baseUrl
_read_tsconfig_aliases joined alias targets onto the tsconfig's own
directory and ignored compilerOptions.baseUrl. In the common monorepo /
NestJS layout (baseUrl "./src" with "@services/*": ["services/*"]), the
alias resolved to <dir>/services instead of <dir>/src/services, so every
aliased import failed to resolve and the import edge was silently dropped
— leaving cross-file caller graphs nearly empty on alias-heavy TS repos.

Resolve `paths` relative to `baseUrl` (TypeScript's actual semantics),
defaulting to "." so configs without baseUrl keep their current behavior.
Add a regression test covering a subdirectory baseUrl; the existing alias
test only exercised baseUrl ".", which is why this slipped through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:20:09 -04:00
Danil Tarasov b525549a24 feat(extract): SystemVerilog class-level extraction; Dart with-clause mixin fix 2026-06-11 19:52:56 +03:00
Skyler Southern 96585badd0 fix: claude-cli backend works headlessly on Windows npm installs
Two Windows fixes for the claude-cli backend:

1. _call_llm (the label/community-naming path) spawned a bare ["claude", ...],
   which CreateProcess cannot resolve to the npm claude.cmd shim (PATHEXT does
   not apply) — every labeling batch failed with WinError 2. Mirror the
   extraction path's resolution: prefer shutil.which("claude.cmd") and pass
   the resolved path. Regression test included.

2. Both claude -p spawn sites now pass CREATE_NO_WINDOW on Windows. Without
   it, each per-batch claude.cmd spawn allocates a console — with Windows
   Terminal as the default terminal, a labeling run pops one visible window
   per batch on the user's desktop for the duration of the model call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 09:58:39 -04:00