fix(cli): survive ktx.yaml version skew and derive repo ownership from disk (#293)

* fix(cli): survive ktx.yaml version skew and derive repo ownership from disk

Loading ktx.yaml is now tolerant of keys this ktx version does not
recognize: they are stripped from the in-memory config (the file on disk
is never rewritten) and reported by ktx status as non-blocking warnings,
while invalid values on recognized fields still fail hard. Repo
ownership is derived from observed state (a .git directory plus a root
ktx.yaml) instead of a ktx.managed git-config marker, so projects
created by any past or future ktx classify identically. initKtxProject
now runs an explicit foreign-repo pre-check and writes ktx.yaml before
initializing git, so an interrupted init leaves only recoverable
residue instead of a bare .git misread as foreign.

* style(cli): trim comment blocks to constraint-only notes

* docs(agents): require constraint-only code comments
This commit is contained in:
Andrey Avtomonov 2026-06-11 22:10:47 +02:00 committed by GitHub
parent a278d2f7d0
commit 0689d709d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 502 additions and 146 deletions

View file

@ -1,6 +1,6 @@
import { promises as fs } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { GitService } from '../../context/core/git.service.js';
import { classifyKtxRepoOwnership, GitService, KtxForeignGitRepositoryError } from '../../context/core/git.service.js';
import { type KtxCoreConfig, type KtxLogger, noopLogger } from '../../context/core/config.js';
import type { KtxProjectConfig } from './config.js';
import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './config.js';
@ -112,14 +112,24 @@ export async function initKtxProject(options: InitKtxProjectOptions): Promise<In
throw new Error(`Project already contains ktx.yaml: ${configPath}`);
}
const config = buildDefaultKtxProjectConfig();
const runtime = await createRuntime(projectDir, config, authorName, authorEmail, logger);
// Must run before ktx.yaml is written: once that file exists the directory
// classifies as ktx-managed, so a foreign repo would be silently adopted.
if ((await classifyKtxRepoOwnership(projectDir)) === 'foreign') {
throw new KtxForeignGitRepositoryError(projectDir);
}
await writeProjectFile(projectDir, 'ktx.yaml', serializeKtxProjectConfig(config));
const config = buildDefaultKtxProjectConfig();
// ktx.yaml (the ownership signal) is written before git init, so an
// interrupted init can never leave a bare `.git` without it — residue that
// would classify as a foreign repo and be unrecoverable.
await fs.mkdir(join(projectDir, '.ktx/cache'), { recursive: true });
for (const file of TRACKED_SCAFFOLD_FILES) {
await writeProjectFile(projectDir, file.path, file.content);
}
await writeProjectFile(projectDir, 'ktx.yaml', serializeKtxProjectConfig(config));
const runtime = await createRuntime(projectDir, config, authorName, authorEmail, logger);
const commit = await runtime.git.commitFiles(
['ktx.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)],