From 185ca49bbaa37a911ec676c025837fd45e96b416 Mon Sep 17 00:00:00 2001 From: jaelgeng Date: Tue, 30 Jun 2026 16:27:59 +0800 Subject: [PATCH 1/2] fix(import): confine teamwiki and docs/team-codebase to .teamai/ directory Previously, `teamai pull` copied teamwiki/ into the project root and `teamai import --from-repo` wrote docs/team-codebase/ into cwd. This polluted the user's working directory with teamai-managed artifacts. Now all knowledge artifacts stay inside .teamai/team-repo/: - Remove teamwiki/ copy-to-cwd in pull - Default import output to team-repo/docs/team-codebase/ - Update recall, codebase-cmd, and lint to read from team-repo path - Fix auto-recall tests that relied on real project config leaking in --story=0 --- src/__tests__/auto-recall.test.ts | 4 ++++ src/__tests__/import-repo-merge.test.ts | 6 +++--- src/codebase-cmd.ts | 13 +++++++++--- src/codebase-wiki-lint.ts | 5 +++-- src/import-repo.ts | 20 +++++++++--------- src/pull.ts | 27 ++----------------------- src/recall.ts | 12 ++++++----- 7 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/__tests__/auto-recall.test.ts b/src/__tests__/auto-recall.test.ts index 8fdecde..de7a67c 100644 --- a/src/__tests__/auto-recall.test.ts +++ b/src/__tests__/auto-recall.test.ts @@ -615,14 +615,18 @@ describe('autoRecallFromInput', () => { let tmpHome: string; const originalHome = process.env.HOME; const originalDisabled = process.env.TEAMAI_RECALL_DISABLED; + let cwdSpy: ReturnType; beforeEach(() => { tmpHome = makeTmpDir(); process.env.HOME = tmpHome; + // Mock cwd so autoDetectInit won't find the real project config + cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpHome); delete process.env.TEAMAI_RECALL_DISABLED; }); afterEach(() => { + cwdSpy.mockRestore(); process.env.HOME = originalHome; if (originalDisabled === undefined) { delete process.env.TEAMAI_RECALL_DISABLED; diff --git a/src/__tests__/import-repo-merge.test.ts b/src/__tests__/import-repo-merge.test.ts index d328f22..7e170ff 100644 --- a/src/__tests__/import-repo-merge.test.ts +++ b/src/__tests__/import-repo-merge.test.ts @@ -87,7 +87,7 @@ describe('importFromRepo — section merge', () => { interactive: false, }); - const repoMdPath = path.join(workdir, 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md'); + const repoMdPath = path.join(workdir, '.teamai', 'team-repo', 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md'); const exists = await fs.pathExists(repoMdPath); expect(exists).toBe(true); @@ -105,7 +105,7 @@ describe('importFromRepo — section merge', () => { interactive: false, }); - const repoMdPath = path.join(workdir, 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md'); + const repoMdPath = path.join(workdir, '.teamai', 'team-repo', 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md'); const stat1 = await fs.stat(repoMdPath); const mtime1 = stat1.mtimeMs; @@ -140,7 +140,7 @@ describe('importFromRepo — section merge', () => { }); it('旧文件含未闭合锚点 → fallback 时备份旧文件、产物使用新 codebase', async () => { - const repoMdPath = path.join(workdir, 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md'); + const repoMdPath = path.join(workdir, '.teamai', 'team-repo', 'docs', 'team-codebase', 'repos', 'github__owner__mergetest.md'); await fs.ensureDir(path.dirname(repoMdPath)); // 准备含未闭合锚点的旧文件 diff --git a/src/codebase-cmd.ts b/src/codebase-cmd.ts index 7df3cf6..fa06f77 100644 --- a/src/codebase-cmd.ts +++ b/src/codebase-cmd.ts @@ -97,12 +97,19 @@ export async function codebaseCmd(opts: CodebaseCmdOptions): Promise { return; } - // 若 teamwiki/ 存在,优先使用图谱 lint + // 若 teamwiki/ 存在(team-repo 内),优先使用图谱 lint const { pathExists } = await import('./utils/fs.js'); - const teamwikiDir = path.join(cwd, 'teamwiki'); + let teamwikiDir: string; + try { + const { autoDetectInit } = await import('./config.js'); + const { localConfig: lc } = await autoDetectInit(); + teamwikiDir = path.join(lc.repo.localPath, 'teamwiki'); + } catch { + teamwikiDir = path.join(cwd, '.teamai', 'team-repo', 'teamwiki'); + } if (await pathExists(teamwikiDir)) { const { lintTeamwiki, formatWikiLintReport } = await import('./codebase-wiki-lint.js'); - const report = await lintTeamwiki({ cwd, severity: opts.severity as 'high' | 'medium' | 'low' | 'info' }); + const report = await lintTeamwiki({ wikiRoot: teamwikiDir, severity: opts.severity as 'high' | 'medium' | 'low' | 'info' }); if (opts.json) { console.log(JSON.stringify(report, null, 2)); } else { diff --git a/src/codebase-wiki-lint.ts b/src/codebase-wiki-lint.ts index 01ff3a8..b927aab 100644 --- a/src/codebase-wiki-lint.ts +++ b/src/codebase-wiki-lint.ts @@ -33,10 +33,11 @@ export interface WikiLintReport { } export async function lintTeamwiki(opts: { - cwd: string; + cwd?: string; + wikiRoot?: string; severity?: WikiLintSeverity; }): Promise { - const wikiRoot = path.join(opts.cwd, 'teamwiki'); + const wikiRoot = opts.wikiRoot ?? path.join(opts.cwd ?? process.cwd(), 'teamwiki'); const issues: WikiLintIssue[] = []; const minSeverity = opts.severity ?? 'info'; const severityOrder: WikiLintSeverity[] = ['info', 'low', 'medium', 'high']; diff --git a/src/import-repo.ts b/src/import-repo.ts index b81a191..f336867 100644 --- a/src/import-repo.ts +++ b/src/import-repo.ts @@ -642,8 +642,18 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise } } + // Resolve team-repo directory (needed for both docs/team-codebase and teamwiki) + let teamRepoDir: string; + try { + const { autoDetectInit } = await import('./config.js'); + const { localConfig: lc } = await autoDetectInit(); + teamRepoDir = lc.repo.localPath; + } catch { + teamRepoDir = path.join(process.cwd(), '.teamai', 'team-repo'); + } + // 4. 写入 docs/team-codebase 叙事文档(AI 扫描成功时) - const outputRoot = output ?? path.join(process.cwd(), 'docs', 'team-codebase'); + const outputRoot = output ?? path.join(teamRepoDir, 'docs', 'team-codebase'); let repoMdPath = path.join(outputRoot, 'repos', `${slug}.md`); if (codebaseMd) { @@ -720,14 +730,6 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise } // end if (codebaseMd) // 4b. 生成 teamwiki/ 知识图谱产物(写入 team-repo 以便自动 push) - let teamRepoDir: string; - try { - const { autoDetectInit } = await import('./config.js'); - const { localConfig: lc } = await autoDetectInit(); - teamRepoDir = lc.repo.localPath; - } catch { - teamRepoDir = path.join(process.cwd(), '.teamai', 'team-repo'); - } const teamwikiRoot = output ? path.resolve(output, '..', 'teamwiki') : path.join(teamRepoDir, 'teamwiki'); diff --git a/src/pull.ts b/src/pull.ts index dabe2a1..aeb9be0 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -618,28 +618,7 @@ async function pullForScope( } } - // Sync teamwiki/ directory (codebase knowledge graph) - const teamwikiRepoDir = path.join(localConfig.repo.localPath, 'teamwiki'); - if (await pathExists(teamwikiRepoDir)) { - const syncTarget = localConfig.projectRoot ?? process.cwd(); - const localTeamwikiDir = path.join(syncTarget, 'teamwiki'); - // 检查本地 graph-index 是否比远端更新(避免覆盖未推送的本地产物) - const localGraph = path.join(localTeamwikiDir, '.indices', 'graph-index.json'); - const remoteGraph = path.join(teamwikiRepoDir, '.indices', 'graph-index.json'); - let shouldSync = true; - if (await pathExists(localGraph) && await pathExists(remoteGraph)) { - const localStat = await fse.stat(localGraph); - const remoteStat = await fse.stat(remoteGraph); - if (localStat.mtimeMs > remoteStat.mtimeMs) { - log.warn(`[${scopeLabel}] 本地 teamwiki/ 比远端更新,跳过覆盖(请先 teamai push)`); - shouldSync = false; - } - } - if (shouldSync) { - await fse.copy(teamwikiRepoDir, localTeamwikiDir, { overwrite: true }); - log.debug(`[${scopeLabel}] Synced teamwiki/ knowledge graph`); - } - } + // teamwiki/ stays inside .teamai/team-repo/ — no copy to project root // Build the index when ANY of the four categories has content. const hasAnySource = @@ -649,10 +628,8 @@ async function pullForScope( await pathExists(skillsRepoDir); // Resolve codebase directory (project cwd or team repo) - const cwdCodebaseDir = path.join(process.cwd(), 'docs', 'team-codebase'); const repoCodebaseDir = path.join(localConfig.repo.localPath, 'docs', 'team-codebase'); - const effectiveCodebaseDir = await pathExists(cwdCodebaseDir) ? cwdCodebaseDir - : await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined; + const effectiveCodebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined; if (hasAnySource || effectiveCodebaseDir) { const votesExist = await pathExists(votesDir); diff --git a/src/recall.ts b/src/recall.ts index ac21149..43e6466 100644 --- a/src/recall.ts +++ b/src/recall.ts @@ -188,10 +188,8 @@ async function loadOrBuildScopeIndex( const docsDir = path.join(localConfig.repo.localPath, 'docs'); const rulesDir = path.join(localConfig.repo.localPath, 'rules'); const skillsDir = path.join(localConfig.repo.localPath, 'skills'); - const cwdCodebaseDir = path.join(process.cwd(), 'docs', 'team-codebase'); const repoCodebaseDir = path.join(localConfig.repo.localPath, 'docs', 'team-codebase'); - const codebaseDir = await pathExists(cwdCodebaseDir) ? cwdCodebaseDir - : await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined; + const codebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : undefined; try { await buildIndex({ learningsDir: effectiveLearningsDir ?? undefined, @@ -259,7 +257,12 @@ export async function recall( log.debug('recall: user scope not available'); } - const hasWiki = await pathExists(path.join(process.cwd(), 'teamwiki')); + // Resolve teamwiki path from team-repo (prefer project scope, fallback to user scope) + const wikiConfig = scopeIndexes[0]?.config; + const wikiRoot = wikiConfig + ? path.join(wikiConfig.repo.localPath, 'teamwiki') + : path.join(process.cwd(), '.teamai', 'team-repo', 'teamwiki'); + const hasWiki = await pathExists(wikiRoot); if (scopeIndexes.length === 0 && !hasWiki) { log.info('No learnings available. Run `teamai pull` first to sync team knowledge.'); return; @@ -281,7 +284,6 @@ export async function recall( } // ── Codebase knowledge graph recall ────────────────────── - const wikiRoot = path.join(process.cwd(), 'teamwiki'); try { const codeResults = await queryCodeKnowledge(query, { wikiRoot, limit: 3, depth: options.depth }); // B11: Normalize BM25 scores to 0-10 range before merging with learnings scores From b6602d874c99b5bfd2d73aab64416f52b379e740 Mon Sep 17 00:00:00 2001 From: jaelgeng Date: Tue, 30 Jun 2026 17:25:58 +0800 Subject: [PATCH 2/2] docs: update outdated output path comments in import-repo --- src/import-repo.ts | 4 ++-- src/utils/team-codebase-paths.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/import-repo.ts b/src/import-repo.ts index f336867..ae42a63 100644 --- a/src/import-repo.ts +++ b/src/import-repo.ts @@ -44,7 +44,7 @@ export interface ImportFromRepoOptions { explicitDomain?: string; /** Dry-run 模式:跳过写盘但执行 clone+扫描 */ dryRun?: boolean; - /** 自定义产物根目录;默认 cwd/docs/team-codebase */ + /** 自定义产物根目录;默认 .teamai/team-repo/docs/team-codebase */ output?: string; /** * 是否启用交互式确认。 @@ -527,7 +527,7 @@ export async function detectDomainDrift(args: { * 1. 解析 url → provider + RepoInfo(owner/repo) * 2. shallow clone(或增量 fetch+reset)到 ~/.teamai/cache/repos/// * 3. generateCodebaseMd({ repoPath: cacheDir }) - * 4. 写出到 /repos/.md(默认 outputRoot=cwd/docs/team-codebase) + * 4. 写出到 /repos/.md(默认 outputRoot=.teamai/team-repo/docs/team-codebase) * 5. 推荐业务域(或使用 --domain 显式指定) * 6. 写入 .teamai/domains.yaml + appendHistory * 7. 写 LAST_SYNC diff --git a/src/utils/team-codebase-paths.ts b/src/utils/team-codebase-paths.ts index 4c3a563..255b206 100644 --- a/src/utils/team-codebase-paths.ts +++ b/src/utils/team-codebase-paths.ts @@ -6,7 +6,7 @@ export const TEAM_CODEBASE_DIR = 'team-codebase'; /** 团队 codebase 各层路径集合。 */ export interface TeamCodebasePaths { - /** /docs/team-codebase */ + /** .teamai/team-repo/docs/team-codebase (or custom --output path) */ root: string; /** /index.md */ index: string;