From 07ab275662e55a750a5d33a7c2dbea6474de2480 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 9 Jun 2026 14:41:43 +0000 Subject: [PATCH 1/5] chore(release): 0.11.0 [skip ci] ## [0.11.0](https://github.com/Kaelio/ktx/compare/v0.10.0...v0.11.0) (2026-06-09) ### Features * **cli:** add Slack community CTA on errors, crashes, setup, and help ([#277](https://github.com/Kaelio/ktx/issues/277)) ([66517fc](https://github.com/Kaelio/ktx/commit/66517fc32018f45e1bb3de70a0d60fecd592edfe)) ### Bug Fixes * **cli:** classify ktx setup abandonment as aborted, not a blank error ([#278](https://github.com/Kaelio/ktx/issues/278)) ([470802e](https://github.com/Kaelio/ktx/commit/470802e58e4cbc2bfdf7e4b8b4adf3b09c93d2f7)) * **cli:** ensure git committer identity during ktx setup ([#276](https://github.com/Kaelio/ktx/issues/276)) ([6b2f7c3](https://github.com/Kaelio/ktx/commit/6b2f7c3365e87aa357fe128b8cd58be51f2cb83b)) ### Documentation * **agents:** sync Opinionated Product Defaults guidance into AGENTS.md ([#280](https://github.com/Kaelio/ktx/issues/280)) ([7b00234](https://github.com/Kaelio/ktx/commit/7b0023471e1bb7e4029346f3c33f4ac8ddf37271)) * align introduction subtitle width with page content ([#275](https://github.com/Kaelio/ktx/issues/275)) ([e5425b5](https://github.com/Kaelio/ktx/commit/e5425b51a3fa1f40210a54984da7ef47a7ca5575)) * consolidate AI Resources into a single page ([#274](https://github.com/Kaelio/ktx/issues/274)) ([8050b59](https://github.com/Kaelio/ktx/commit/8050b59f6ebfc5d0ff1347ed1bfdc3bdb45a7e15)) * document upgrading to the latest ktx version ([#273](https://github.com/Kaelio/ktx/issues/273)) ([7ece0b6](https://github.com/Kaelio/ktx/commit/7ece0b63d339288ee6e008f0d3a8131828440a09)) * remove product switcher from docs nav ([#272](https://github.com/Kaelio/ktx/issues/272)) ([07bbdef](https://github.com/Kaelio/ktx/commit/07bbdefa14852d454e4d70391316905149c4af82)) ### Other Changes * refresh star history chart [skip ci] ([bd3a375](https://github.com/Kaelio/ktx/commit/bd3a37508182830dc25e9cf4e6b72eef804e76fc)) * refresh star history chart [skip ci] ([50dec7b](https://github.com/Kaelio/ktx/commit/50dec7bf64e2833e358b36e12d30262d60558f1c)) --- package.json | 2 +- packages/cli/package.json | 2 +- python/ktx-daemon/pyproject.toml | 2 +- python/ktx-sl/pyproject.toml | 2 +- release-policy.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 10643e11..4b75c0a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ktx-workspace", - "version": "0.10.0", + "version": "0.11.0", "description": "Workspace root for ktx packages", "private": true, "type": "module", diff --git a/packages/cli/package.json b/packages/cli/package.json index 85695a8a..9bb4c5a1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@kaelio/ktx", - "version": "0.10.0", + "version": "0.11.0", "description": "Standalone ktx context layer for data agents", "author": { "name": "Kaelio", diff --git a/python/ktx-daemon/pyproject.toml b/python/ktx-daemon/pyproject.toml index b7e9fc99..3bbb3f60 100644 --- a/python/ktx-daemon/pyproject.toml +++ b/python/ktx-daemon/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ktx-daemon" -version = "0.10.0" +version = "0.11.0" description = "Portable compute package for KTX semantic-layer operations" readme = "README.md" requires-python = ">=3.13" diff --git a/python/ktx-sl/pyproject.toml b/python/ktx-sl/pyproject.toml index dae1f42a..83fcb6f4 100644 --- a/python/ktx-sl/pyproject.toml +++ b/python/ktx-sl/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ktx-sl" -version = "0.10.0" +version = "0.11.0" description = "Agent-first semantic layer engine with aggregate locality" readme = "README.md" requires-python = ">=3.13" diff --git a/release-policy.json b/release-policy.json index 6c9d2d4b..35a2d1d0 100644 --- a/release-policy.json +++ b/release-policy.json @@ -19,7 +19,7 @@ }, "publishedPackageSmoke": { "packageName": "@kaelio/ktx", - "version": "0.10.0", + "version": "0.11.0", "registry": null }, "runtimeInstaller": { From 9ff0e86bb85fcbd69f63b6c77847837d7c7ed468 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 9 Jun 2026 17:24:43 +0200 Subject: [PATCH 2/5] chore: remove dead pnpm.onlyBuiltDependencies from package.json pnpm 11 no longer reads the package.json "pnpm" field and warns on it. The build allowlist already lives in pnpm-workspace.yaml via allowBuilds (better-sqlite3, esbuild, sharp), so this block was redundant noise. --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index 4b75c0a7..f941241a 100644 --- a/package.json +++ b/package.json @@ -69,11 +69,6 @@ "typescript": "^6.0.3", "yaml": "^2.9.0" }, - "pnpm": { - "onlyBuiltDependencies": [ - "better-sqlite3" - ] - }, "license": "Apache-2.0", "repository": { "type": "git", From 0425160857123534c7e5f968f3c1d1b81403bf9b Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 9 Jun 2026 19:11:39 +0200 Subject: [PATCH 3/5] fix(cli): clear error when ktx setup has no LLM backend under --no-input (#281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cli): fail clearly when ktx setup has no LLM backend under --no-input Non-interactive `ktx setup` silently defaulted the LLM backend to `anthropic` and then failed with `Missing Anthropic API key: pass --anthropic-api-key-env or --anthropic-api-key-file` — confusing for users who selected a different provider (e.g. `--target claude-code`) and never asked for the Anthropic API backend. That silent default could never succeed: it was reached only when no backend, Anthropic key, or Vertex flag was supplied, and in exactly that case the Anthropic credential resolver always failed (no env fallback in disabled mode). Unlike embeddings, the LLM has no credential-free default (anthropic needs a key, vertex needs gcloud ADC, claude-code/codex need a logged-in local CLI), so there is nothing safe to assume. `chooseBackend` now fails clearly in disabled mode with no backend, naming the (hidden) `--llm-backend` flag and its choices and noting each backend's credential needs. `--llm-backend` stays hidden in `--help`, consistent with the rest of the documented automation surface; the error message is the discovery path. - Add a unit test (no backend, disabled -> clear message) and a CLI/integration test (`--target claude-code --no-input` -> exit 1, clear message, not the Anthropic red herring). - Document the no-default behavior and add a Common-errors row in docs-site ktx-setup.mdx. * refactor(cli): single source of truth for setup LLM backends The set of LLM backends a user can pick during `ktx setup` (claude-code, codex, anthropic, vertex) was hand-enumerated in five places: the `--llm-backend` arg parser, the `KtxSetupLlmBackend` union, the interactive prompt's narrowing, the prompt options, and the missing-backend error. Only some had TypeScript coverage, so adding a backend could silently drift (e.g. a valid value rejected by the parser, or routed to anthropic by the prompt's `? : 'anthropic'` fallback). Collapse them onto one `KTX_SETUP_LLM_BACKENDS` list: - `KtxSetupLlmBackend` is derived from it. - `isKtxSetupLlmBackend` is the shared validator; the arg parser and the prompt both route through it instead of re-listing literals. - The prompt options derive from the list, with a `Record` label map so a new backend fails to compile until it has a label. - The missing-backend error builds its choice list from the same source. Behavior-preserving: identical accepted values and parse error, identical prompt options (asserted by an existing test), and the prompt's unreachable fallback now cancels rather than silently assuming anthropic. --- .../content/docs/cli-reference/ktx-setup.mdx | 8 +++ packages/cli/src/commands/setup-commands.ts | 4 +- packages/cli/src/setup-models.ts | 55 +++++++++++++++---- packages/cli/test/setup-models.test.ts | 17 ++++-- packages/cli/test/setup.test.ts | 32 +++++++++++ 5 files changed, 97 insertions(+), 19 deletions(-) diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx index 8ae4469d..f75fdf46 100644 --- a/docs-site/content/docs/cli-reference/ktx-setup.mdx +++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx @@ -67,6 +67,13 @@ of Anthropic API key or Vertex flags. After you choose a backend, `ktx setup` writes that backend's per-role model preset to `ktx.yaml`. To change a model, edit the matching `llm.models.` value in `ktx.yaml`. +With `--no-input`, `ktx setup` does not assume a default LLM provider, because +every backend needs credentials only you can supply. Pass `--llm-backend` +explicitly. Note that `--target` selects the agent integration, not the LLM +provider: `ktx setup --target claude-code --no-input` still needs +`--llm-backend claude-code` to use your Claude subscription for **ktx** LLM +calls. + ### Embeddings | Flag | Description | @@ -276,6 +283,7 @@ Use `ktx status` for repeatable readiness checks after setup exits. |-------|-------|----------| | Setup resumes an unexpected project | `KTX_PROJECT_DIR` or nearest `ktx.yaml` points to another directory | Pass `--project-dir ` explicitly | | Setup cannot run in CI | Required values are missing and `--no-input` disables prompts | Provide the relevant automation flags or create a fixture `ktx.yaml` | +| `Missing LLM backend: pass --llm-backend …` | `--no-input` setup ran without an LLM backend; `--target` does not select one | Pass `--llm-backend claude-code`, `codex`, `anthropic`, or `vertex` (with that backend's credential flags) | | Provider health check fails | Provider key, model id, Vertex project, or Vertex location is invalid | Fix the `env:` or `file:` reference and rerun setup | | Python runtime is missing | The selected setup needs runtime-backed agent, query-history, Looker, or local embedding features | Accept the interactive prompt, rerun with `--yes`, or run the suggested `ktx admin runtime install` command | | `--enable-query-history` is rejected | The selected database driver does not support query history | Use Postgres, BigQuery, or Snowflake, or rerun without query-history flags | diff --git a/packages/cli/src/commands/setup-commands.ts b/packages/cli/src/commands/setup-commands.ts index 418b27f9..e8510210 100644 --- a/packages/cli/src/commands/setup-commands.ts +++ b/packages/cli/src/commands/setup-commands.ts @@ -2,7 +2,7 @@ import { type Command, InvalidArgumentError, Option } from '@commander-js/extra- import type { KtxCliCommandContext } from '../cli-program.js'; import { resolveCommandProjectDir } from '../cli-program.js'; import type { KtxSetupDatabaseDriver } from '../setup-databases.js'; -import type { KtxSetupLlmBackend } from '../setup-models.js'; +import { isKtxSetupLlmBackend, type KtxSetupLlmBackend } from '../setup-models.js'; import type { KtxSetupSourceType } from '../setup-sources.js'; async function runSetupArgs( @@ -29,7 +29,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' { } function llmBackend(value: string): KtxSetupLlmBackend { - if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') { + if (isKtxSetupLlmBackend(value)) { return value; } throw new InvalidArgumentError(`invalid choice '${value}'`); diff --git a/packages/cli/src/setup-models.ts b/packages/cli/src/setup-models.ts index 911579a9..fbbabbdb 100644 --- a/packages/cli/src/setup-models.ts +++ b/packages/cli/src/setup-models.ts @@ -51,7 +51,27 @@ export type KtxSetupModelResult = | { status: 'missing-input'; projectDir: string } | { status: 'failed'; projectDir: string }; -export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code' | 'codex'; +// Single source of truth for the LLM backends a user can pick during setup. +// The CLI arg parser, the interactive prompt, and the missing-backend error all +// derive from this list, so adding a backend is one edit. Order is the prompt's +// preference order (subscription backends first). +const KTX_SETUP_LLM_BACKENDS = ['claude-code', 'codex', 'anthropic', 'vertex'] as const; +export type KtxSetupLlmBackend = (typeof KTX_SETUP_LLM_BACKENDS)[number]; + +/** Validates a raw CLI or prompt value against the setup-selectable LLM backends. */ +export function isKtxSetupLlmBackend(value: string): value is KtxSetupLlmBackend { + return KTX_SETUP_LLM_BACKENDS.some((backend) => backend === value); +} + +// Display labels for the interactive provider prompt. The Record key type forces +// every backend to carry a label, so adding one to KTX_SETUP_LLM_BACKENDS fails +// to compile until its prompt option exists here. +const KTX_SETUP_LLM_BACKEND_LABELS: Record = { + 'claude-code': 'Claude subscription (Pro/Max)', + codex: 'Codex subscription', + anthropic: 'Anthropic API key', + vertex: 'Google Vertex AI for Anthropic Claude', +}; /** @internal */ export interface KtxSetupModelPromptAdapter { @@ -135,7 +155,18 @@ const execFileAsync = promisify(execFile); type ChooseBackendResult = | { status: 'ready'; backend: KtxSetupLlmBackend; prompted: boolean } - | { status: 'back' }; + | { status: 'back' } + | { status: 'missing-input' }; + +// Non-interactive setup cannot pick a provider safely: every backend needs +// something the user must supply (an API key, gcloud ADC, or a logged-in local +// CLI), so there is no credential-free default to fall back to. Name the hidden +// --llm-backend flag and its choices here instead, mirroring how the other +// automation errors guide users to the flag they need. +const MISSING_LLM_BACKEND_MESSAGE = + `Missing LLM backend: pass --llm-backend with one of ${KTX_SETUP_LLM_BACKENDS.join(', ')}. ` + + 'claude-code and codex use local CLI authentication; anthropic also needs --anthropic-api-key-env or ' + + '--anthropic-api-key-file, and vertex also needs --vertex-project.'; type VertexConfigChoice = | { @@ -446,7 +477,8 @@ async function chooseBackend( return { status: 'ready', backend: explicit, prompted: false }; } if (args.inputMode === 'disabled') { - return { status: 'ready', backend: 'anthropic', prompted: false }; + io.stderr.write(`${MISSING_LLM_BACKEND_MESSAGE}\n`); + return { status: 'missing-input' }; } const prompts = deps.prompts ?? createPromptAdapter(); @@ -458,21 +490,20 @@ async function chooseBackend( const choice = await prompts.select({ message: 'Which LLM provider should KTX use?', options: [ - { value: 'claude-code', label: 'Claude subscription (Pro/Max)' }, - { value: 'codex', label: 'Codex subscription' }, - { value: 'anthropic', label: 'Anthropic API key' }, - { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' }, + ...KTX_SETUP_LLM_BACKENDS.map((backend) => ({ value: backend, label: KTX_SETUP_LLM_BACKEND_LABELS[backend] })), { value: 'back', label: 'Back' }, ], }); if (choice === 'back') { return { status: 'back' }; } - return { - status: 'ready', - backend: choice === 'vertex' || choice === 'claude-code' || choice === 'codex' ? choice : 'anthropic', - prompted: true, - }; + if (isKtxSetupLlmBackend(choice)) { + return { status: 'ready', backend: choice, prompted: true }; + } + // Options are derived from KTX_SETUP_LLM_BACKENDS, so the only other value is + // 'back' (handled above). Treat any unexpected value as a cancel rather than + // silently assuming a provider. + return { status: 'back' }; } function resolveProvidedVertexRef( diff --git a/packages/cli/test/setup-models.test.ts b/packages/cli/test/setup-models.test.ts index f09691e0..ba5ce21f 100644 --- a/packages/cli/test/setup-models.test.ts +++ b/packages/cli/test/setup-models.test.ts @@ -814,7 +814,7 @@ describe('setup Anthropic model step', () => { expect(io.stderr()).toContain(`Missing Anthropic API key file: ${missingSecretPath}`); }); - it('does not recommend skipping when non-interactive setup is missing an Anthropic credential source', async () => { + it('fails clearly when non-interactive setup has no LLM backend instead of assuming Anthropic', async () => { const io = makeIo(); const result = await runKtxSetupAnthropicModelStep( @@ -823,10 +823,17 @@ describe('setup Anthropic model step', () => { ); expect(result.status).toBe('missing-input'); - expect(io.stderr()).toContain( - 'Missing Anthropic API key: pass --anthropic-api-key-env or --anthropic-api-key-file.', - ); - expect(io.stderr()).not.toContain('--skip-llm'); + const stderr = io.stderr(); + expect(stderr).toContain('Missing LLM backend: pass --llm-backend'); + // Names every backend so the user can choose without reading hidden --help flags. + expect(stderr).toContain('claude-code'); + expect(stderr).toContain('codex'); + expect(stderr).toContain('anthropic'); + expect(stderr).toContain('vertex'); + // Does not mislead with an Anthropic-key error the user never opted into. + expect(stderr).not.toContain('Missing Anthropic API key'); + // Does not nudge users to skip the LLM. + expect(stderr).not.toContain('--skip-llm'); }); it('writes pasted keys to .ktx/secrets and never prints the key', async () => { diff --git a/packages/cli/test/setup.test.ts b/packages/cli/test/setup.test.ts index 04ca32d1..546136fa 100644 --- a/packages/cli/test/setup.test.ts +++ b/packages/cli/test/setup.test.ts @@ -664,6 +664,38 @@ describe('setup status', () => { expect(testIo.stderr()).toBe(''); }); + it('fails clearly when a non-interactive run has an agent target but no LLM backend', async () => { + const testIo = makeIo(); + + // --target selects agent integration, not the LLM provider. A non-interactive + // run with no --llm-backend must say so plainly instead of assuming Anthropic. + await expect( + runKtxSetup( + { + command: 'run', + projectDir: tempDir, + mode: 'auto', + agents: false, + target: 'claude-code', + skipAgents: true, + inputMode: 'disabled', + yes: true, + cliVersion: '0.2.0', + skipLlm: false, + skipEmbeddings: true, + skipDatabases: true, + skipSources: true, + databaseSchemas: [], + }, + testIo.io, + ), + ).resolves.toBe(1); + + const stderr = testIo.stderr(); + expect(stderr).toContain('Missing LLM backend: pass --llm-backend'); + expect(stderr).not.toContain('Missing Anthropic API key'); + }); + it('preserves a newly created missing project directory when a later setup step fails', async () => { const projectDir = join(tempDir, 'missing-project'); const testIo = makeIo(); From 65de75ebd770d97dae3484b18facafe6a74c6540 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:49:48 +0000 Subject: [PATCH 4/5] chore: refresh star history chart [skip ci] --- assets/star-history.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/star-history.svg b/assets/star-history.svg index 87592b49..8fa7bc08 100644 --- a/assets/star-history.svg +++ b/assets/star-history.svg @@ -1 +1 @@ -star-history.comMay 17May 24May 31Jun 07 200400600800kaelio/ktxStar HistoryDateGitHub Stars +star-history.comMay 17May 24May 31Jun 07 200400600800kaelio/ktxStar HistoryDateGitHub Stars From fd18caa26ac78c8994528ff55dac3c900ae6eae9 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 9 Jun 2026 23:37:24 +0200 Subject: [PATCH 5/5] fix(cli): own a dedicated git repo at the project dir when nested in an enclosing repo (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitService.initialize() used checkIsRepo(), which is true whenever the project dir sits anywhere inside a git working tree. So when a ktx project lived in a subdirectory of an enclosing repo, ktx skipped `git init` and silently adopted the enclosing repo as its store. Every ktx relative path assumes the project dir IS the working-tree root. During ingest, wiki/SL pages are written through a session worktree (whose root is the worktree dir, so the page is recorded at repo-relative `wiki/global/.md`) and then squash-merged into the main worktree. With an adopted enclosing repo, the main worktree's root is the enclosing git root, so the merge wrote the page to `/wiki/global/` — outside the project dir. reindex scans `/wiki/global/`, found nothing, and wiki_search silently returned empty (knowledge_pages = 0) even though ingest reported success. Detect the project dir's own root with checkIsRepo(IS_REPO_ROOT) and initialize a dedicated repo there unless the project dir is already a repo root. This keeps adopting a user-created repo when the project dir IS that repo's root, fixes the silent wiki/SL/memory divergence at its source for every writer, and stops ktx from committing its scaffold into the user's enclosing repo. Regression tests cover both layers: a project nested in an enclosing repo gets its own .git (and the enclosing repo stays untouched), and a wiki page written through a session worktree + squash-merge lands in the project dir and is discovered by reindex. --- packages/cli/src/context/core/git.service.ts | 13 ++-- .../reindex.nested-git-root.test.ts | 66 +++++++++++++++++++ .../cli/test/context/project/project.test.ts | 27 +++++++- 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts diff --git a/packages/cli/src/context/core/git.service.ts b/packages/cli/src/context/core/git.service.ts index 216c5460..a9638ea5 100644 --- a/packages/cli/src/context/core/git.service.ts +++ b/packages/cli/src/context/core/git.service.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'node:fs'; import { dirname, join } from 'node:path'; -import type { SimpleGit } from 'simple-git'; +import { CheckRepoActions, type SimpleGit } from 'simple-git'; import { noopLogger, resolveConfigDir, type KtxCoreConfig, type KtxLogger } from './config.js'; import { createSimpleGit } from './git-env.js'; @@ -98,10 +98,15 @@ export class GitService { private async initialize(): Promise { try { - // Check if already initialized - const isRepo = await this.git.checkIsRepo(); + // Adopt an existing repo ONLY when this directory is itself that repo's root. + // When it sits below an enclosing repo, a plain checkIsRepo() is true and ktx + // would silently piggyback on the enclosing tree — but every ktx relative path + // (file-store writes, session worktrees, squash-merges, reindex scans) assumes + // this directory IS the working-tree root. So treat "inside an enclosing repo" + // the same as "no repo" and initialize a dedicated repo rooted here. + const isRepoRoot = await this.git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); - if (!isRepo) { + if (!isRepoRoot) { await this.git.init(); this.logger.log('Initialized git repository'); } diff --git a/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts b/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts new file mode 100644 index 00000000..97efa993 --- /dev/null +++ b/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts @@ -0,0 +1,66 @@ +import { execFileSync } from 'node:child_process'; +import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { reindexLocalIndexes } from '../../../src/context/index-sync/reindex.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; + +const AUTHOR = 'Agent'; +const EMAIL = 'agent@example.com'; + +const WIKI_PAGE = '---\nsummary: Revenue\nusage_mode: auto\n---\n\nPaid orders.\n'; + +/** + * Regression for the "wiki silently unsearchable when the project dir is not the git root" + * bug: a ktx project initialized below an existing git working tree. ingest writes wiki + * pages through a session worktree and squash-merges into main, so the page must land + * inside the project dir (where reindex scans), not at the enclosing git root. + */ +describe('reindex with a ktx project nested inside an enclosing git repo', () => { + let tempDir: string; + let enclosing: string; + let projectDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'ktx-nested-git-root-')); + enclosing = join(tempDir, 'enclosing'); + await mkdir(enclosing, { recursive: true }); + execFileSync('git', ['init', '-q'], { cwd: enclosing }); + projectDir = join(enclosing, 'analytics'); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + it('indexes a wiki page written through a session worktree and squash-merged into main', async () => { + const project: KtxLocalProject = await initKtxProject({ + projectDir, + authorName: AUTHOR, + authorEmail: EMAIL, + }); + + // Mirror the ingest write path: create a session worktree, write the page on its + // branch through the worktree-scoped file store, then squash-merge into main. + const mainHead = await project.git.revParseHead(); + const workdir = join(projectDir, '.ktx/worktrees/session-test'); + const branch = 'session/test'; + await project.git.addWorktree(workdir, branch, mainHead); + const worktreeStore = project.fileStore.forWorktree(workdir); + await worktreeStore.writeFile('wiki/global/revenue.md', WIKI_PAGE, AUTHOR, EMAIL, 'Add revenue page'); + const merge = await project.git.squashMergeIntoMain(branch, AUTHOR, EMAIL, 'Merge session'); + expect(merge.ok).toBe(true); + await project.git.removeWorktree(workdir); + await project.git.deleteBranch(branch, true); + + // The page must land inside the project dir, not the enclosing git root. + await expect(stat(join(projectDir, 'wiki/global/revenue.md'))).resolves.toBeDefined(); + await expect(stat(join(enclosing, 'wiki/global/revenue.md'))).rejects.toMatchObject({ code: 'ENOENT' }); + + // ...and reindex must discover and index it. + const summary = await reindexLocalIndexes(project, { force: false, embeddingService: null }); + const global = summary.scopes.find((scope) => scope.label === 'global'); + expect(global).toMatchObject({ scanned: 1, updated: 1 }); + }); +}); diff --git a/packages/cli/test/context/project/project.test.ts b/packages/cli/test/context/project/project.test.ts index 668fa264..1027d174 100644 --- a/packages/cli/test/context/project/project.test.ts +++ b/packages/cli/test/context/project/project.test.ts @@ -1,4 +1,5 @@ -import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -60,6 +61,30 @@ describe('KTX local project runtime', () => { }); }); + it('initializes a dedicated git repo at the project dir even when nested inside an enclosing repo', async () => { + // A ktx project dir living below an existing git working tree (e.g. an analytics + // subfolder of an app repo). ktx must own its own repo rooted at the project dir, + // not silently adopt the enclosing repo — otherwise worktree writes resolve against + // the enclosing root and land outside the project dir. + const enclosing = join(tempDir, 'enclosing'); + await mkdir(enclosing, { recursive: true }); + execFileSync('git', ['init', '-q'], { cwd: enclosing }); + + const projectDir = join(enclosing, 'analytics'); + await initKtxProject({ projectDir, authorName: 'Agent', authorEmail: 'agent@example.com' }); + + await expect(stat(join(projectDir, '.git'))).resolves.toBeDefined(); + const toplevel = execFileSync('git', ['rev-parse', '--show-toplevel'], { + cwd: projectDir, + encoding: 'utf-8', + }).trim(); + expect(await realpath(toplevel)).toBe(await realpath(projectDir)); + + // ktx must not write its scaffold commits into the user's enclosing repo. + const enclosingTracked = execFileSync('git', ['ls-files'], { cwd: enclosing, encoding: 'utf-8' }); + expect(enclosingTracked).not.toContain('ktx.yaml'); + }); + it('rejects reinitializing an existing project unless force is set', async () => { const projectDir = join(tempDir, 'warehouse'); await initKtxProject({ projectDir });