From 9154e29b823fe006b88dd359c75b442b748fab7e Mon Sep 17 00:00:00 2001 From: wojiecihuo <91608029+zszz3@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:51:57 +0800 Subject: [PATCH 1/3] feat(contribute): select an active learnings namespace (#916) --- docs/designs/multi-project-management.md | 12 +- docs/usage-guide.md | 16 +- docs/usage-guide.zh-CN.md | 13 +- skill-data/core/references/commands.md | 3 +- skill-data/share/SKILL.md | 6 +- .../e2e/contribute-namespace.test.ts | 184 ++++++++++++++++++ src/contribute.ts | 28 ++- src/index.ts | 3 +- src/projects-cmd.ts | 6 + 9 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 src/__tests__/e2e/contribute-namespace.test.ts diff --git a/docs/designs/multi-project-management.md b/docs/designs/multi-project-management.md index 027c79148..0d4bcaa05 100644 --- a/docs/designs/multi-project-management.md +++ b/docs/designs/multi-project-management.md @@ -537,7 +537,10 @@ as "changed by you (kept by pull)". skills/rules/claudemd by the union; namespace-aware learnings sync + cleanup (`src/pull.ts:687-745`, which today copies the whole flat `learnings/`). - `src/push.ts` — `--project` landing point. -- `src/contribute.ts` — landing-point priority (active project namespace → root). +- `src/contribute.ts` — explicit `--namespace` must belong to the active projects' + learnings namespaces; otherwise default to the only active namespace or the + shared root. Persist the chosen relative path in the existing pending queue + so retries keep their destination. `projects list` shows the default and choices. - `src/utils/search-index.ts` — a namespace-aware learnings collector (root + active project subdirs), replacing the flat `collectFlatMdEntries` call at `src/utils/search-index.ts:549`. @@ -604,8 +607,11 @@ works. 5. **Learnings isolation (P2 core).** A learning contributed under `hai-inference` does **not** appear in dir B's `teamai recall`; a root-level learning appears in both. -6. **Contribute landing.** `teamai contribute` in dir A lands under - `learnings/hai-inference/`; with no active project it lands at the root. +6. **Contribute landing.** `teamai contribute` defaults to the only active + learnings namespace, or the root when there are none or several. With + `--namespace`, it accepts only an active learnings namespace (not necessarily + a project id); unavailable paths are refused before queueing. Preview and + offline retry retain the selected destination. 7. **Namespace-aware index.** `teamai recall` in dir A scans root + `hai-inference/` subdir (proves the flat→recursive collector change). 8. **Member roster append.** `init` in both dirs → `members/.yaml` lists diff --git a/docs/usage-guide.md b/docs/usage-guide.md index d16b5ba08..3253e4e01 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -323,8 +323,10 @@ explicit `--project` skips the picker. - **Backward compatible.** A repo without `manifest/projects.yaml` behaves exactly as before; existing flat `learnings/*.md` stay shared with everyone (zero migration). -- **`teamai contribute`** lands a learning under the active project's subdirectory - when exactly one project is active, otherwise at the shared root. +- **`teamai contribute`** defaults to `learnings//` when the active + projects resolve to exactly one learnings namespace, otherwise to the shared + root. Pass `--namespace ` to choose one of those active namespaces; + `teamai projects list` shows the default destination and accepted namespaces. `manifest/projects.yaml` example: @@ -1387,8 +1389,16 @@ You can also specify a file manually: ```bash teamai contribute --file /tmp/session.md teamai contribute --file /tmp/session.md --scope project +teamai contribute --file /tmp/session.md --namespace payments ``` +`--namespace` accepts only the selected scope's active learnings namespaces from +`manifest/projects.yaml`, which can differ from project ids. An unavailable or +unsafe namespace is rejected before the learning is queued. Without the flag, +the default above is unchanged; when several namespaces are active the command +lists them and explains how to choose one. `--dry-run` previews the selected +path without writing, and an offline contribution keeps that path when retried. + #### Turning the hint off Teams that route knowledge sharing through their own review flow (for example, a personal retrospective that opens ordinary PRs) can switch the hint off without touching the rest of the Stop hook — update checks, votes sync, and dashboard reporting keep running. Same two-tier pattern as recall: @@ -1922,7 +1932,7 @@ teamai import --from-repo https://github.com/org/repo --skip-enrich If core graph extraction or writing fails, the import reports an error without marking the commit as synced. The next incremental run retries that commit. -`--from-mr` publishes its learning the way `teamai contribute` does, on the `teamai-learnings` branch: under `learnings//` when exactly one active project declares a learnings namespace, otherwise at the shared `learnings/` root. If that fails, the learning stays queued on this machine and the next `teamai pull` publishes it; when a learnings checkout teamai refuses stopped it, no pull can until you deal with that checkout as the message says. +`--from-mr` publishes its learning the way `teamai contribute` does by default, on the `teamai-learnings` branch: under `learnings//` when the active projects resolve to exactly one learnings namespace, otherwise at the shared `learnings/` root. If that fails, the learning stays queued on this machine and the next `teamai pull` publishes it; when a learnings checkout teamai refuses stopped it, no pull can until you deal with that checkout as the message says. When the draft overlaps existing learnings, from the shared root or your active projects' namespaces, the command names them (`Possible duplicate: this learning overlaps N existing learning(s): .`), with `--all` too. It is a notice only: nothing is marked or replaced. When `manifest/projects.yaml` cannot be read, the check compares the shared root only and says so. diff --git a/docs/usage-guide.zh-CN.md b/docs/usage-guide.zh-CN.md index 498229d9e..b296eb496 100644 --- a/docs/usage-guide.zh-CN.md +++ b/docs/usage-guide.zh-CN.md @@ -279,8 +279,9 @@ cd ~/work/billing && teamai init --project billing `teamai projects set all` 走的是字面 id,仍能单独激活它。 - **向后兼容。** 没有 `manifest/projects.yaml` 的仓库行为与之前完全一致;现存扁平 的 `learnings/*.md` 继续对所有人共享(零迁移)。 -- **`teamai contribute`** 在恰好激活一个项目时,把经验落到该项目子目录,否则落到 - 共享的根目录。 +- **`teamai contribute`** 在激活项目合计解析出恰好一个 learnings namespace 时, + 默认写入 `learnings//`,否则写入共享根目录。可用 `--namespace ` + 指定其中一个活跃 namespace;`teamai projects list` 会显示默认落点和允许的选项。 `manifest/projects.yaml` 示例: @@ -1245,8 +1246,14 @@ Consider running `/teamai share what this session taught me` to summarize what y ```bash teamai contribute --file /tmp/session.md teamai contribute --file /tmp/session.md --scope project +teamai contribute --file /tmp/session.md --namespace payments ``` +`--namespace` 只接受所选 scope 在 `manifest/projects.yaml` 中声明的活跃 learnings +namespace,它不一定等于项目 id。不可用或不安全的 namespace 会在经验入队前被拒绝。 +不传该参数时保持上述默认行为;存在多个 namespace 时,命令会列出它们并提示如何选择。 +`--dry-run` 只预览选中的路径,不写入;离线贡献在重试发布时仍保留该路径。 + #### 关闭提醒 如果团队通过自己的评审流程沉淀知识(例如个人复盘后提交普通 PR),可以只关闭这条提醒,Stop hook 的其余功能(更新检查、votes 同步、dashboard 上报)照常运行。配置方式与 recall 相同,分两层: @@ -1767,7 +1774,7 @@ teamai import --from-repo https://github.com/org/repo --skip-enrich 如果核心知识图谱提取或写入失败,导入会报错,且不会将该提交标记为已同步。下次增量导入会重试该提交。 -`--from-mr` 与 `teamai contribute` 一样,把提取的经验发布到 `teamai-learnings` 分支:恰好一个激活项目声明了 learnings namespace 时放在 `learnings//` 下,否则放在共享的 `learnings/` 根目录。发布失败时,经验留在本机队列中,下次 `teamai pull` 会发布它;若阻止发布的是 teamai 拒绝使用的 learnings 检出,则在你按提示处理该检出之前,任何 pull 都无法发布它。 +`--from-mr` 与 `teamai contribute` 的默认行为一样,把提取的经验发布到 `teamai-learnings` 分支:激活项目合计解析出恰好一个 learnings namespace 时放在 `learnings//` 下,否则放在共享的 `learnings/` 根目录。发布失败时,经验留在本机队列中,下次 `teamai pull` 会发布它;若阻止发布的是 teamai 拒绝使用的 learnings 检出,则在你按提示处理该检出之前,任何 pull 都无法发布它。 如果草稿与已有经验(共享根目录或当前激活项目的 namespace 中的)高度重叠,命令会列出这些文件(`Possible duplicate: this learning overlaps N existing learning(s): .`),使用 `--all` 时同样如此。这只是提示:不会标记或替换任何已有经验。`manifest/projects.yaml` 无法读取时,只与共享根目录比较,并给出提示。 diff --git a/skill-data/core/references/commands.md b/skill-data/core/references/commands.md index 0cc5ed3eb..9b46ab27e 100644 --- a/skill-data/core/references/commands.md +++ b/skill-data/core/references/commands.md @@ -295,9 +295,10 @@ Generated: do not edit by hand. Regenerate with ## contribute -- `teamai contribute` — Contribute session knowledge to team repo +- `teamai contribute` — Contribute session knowledge to team repo; projects list shows the default destination and allowed namespaces - `--file ` — Path to the contribution document - `--title ` — Title for the contribution document + - `--namespace <ns>` — Write to an active learnings namespace (default: the only active namespace, or the shared root when none or several are active) - `--session-id <id>` — Session ID for dedup tracking - `--scope <scope>` — Target scope: user or project diff --git a/skill-data/share/SKILL.md b/skill-data/share/SKILL.md index 12fb76c1f..fe54e33e2 100644 --- a/skill-data/share/SKILL.md +++ b/skill-data/share/SKILL.md @@ -30,7 +30,9 @@ URLs, paths and code identifiers stay as they are. - Which tools or skills proved especially useful - Pitfalls and things to watch out for 3. **Save it**: write the document to a temporary file -4. **Push it to the team**: run `teamai contribute --file <path> --title "<title>"` +4. **Choose the destination**: in the directory you worked in, run `teamai projects list`. + Keep the displayed default or choose one of its accepted namespaces; see `teamai contribute --help` for options. +5. **Push it to the team**: run `teamai contribute --file <path> --title "<title>"` in that directory, using the destination you chose. ## Document Template @@ -47,7 +49,7 @@ teamai contribute --file /tmp/session-summary.md --title "Debugging K8s pod star ## Important -- Run this as a **sub-agent** (Agent tool) to avoid polluting the main session's context +- Run this as a **sub-agent** (Agent tool) to avoid polluting the main session's context; pass it the directory you worked in so it uses that directory's destinations - The document is pushed to the team repo's `teamai-learnings` branch, under `learnings/`, with no pull request - Team members will see it on their next `teamai pull` - Keep summaries concise and actionable — this is a knowledge base, not a diary diff --git a/src/__tests__/e2e/contribute-namespace.test.ts b/src/__tests__/e2e/contribute-namespace.test.ts new file mode 100644 index 000000000..030d1c9ed --- /dev/null +++ b/src/__tests__/e2e/contribute-namespace.test.ts @@ -0,0 +1,184 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import YAML from 'yaml'; + +const cli = fileURLToPath(new URL('../../../dist/index.js', import.meta.url)); +let sandbox: string; +let home: string; +let project: string; +let origin: string; +let clone: string; +let configPath: string; +let note: string; +let env: NodeJS.ProcessEnv; + +function git(args: string[], cwd = sandbox): string { + return execFileSync('git', args, { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); +} + +function run(args: string[]) { + const result = spawnSync(process.execPath, [cli, ...args], { + cwd: project, env, encoding: 'utf8', timeout: 30_000, + }); + if (result.error) throw result.error; + return { code: result.status, output: `${result.stdout}${result.stderr}` }; +} + +function writeYaml(file: string, value: unknown): void { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, YAML.stringify(value)); +} + +function setProjects(projects: string[]): void { + const config = YAML.parse(fs.readFileSync(configPath, 'utf8')); + writeYaml(configPath, { ...config, projects }); +} + +// Includes directory creation as well as byte changes, including the queue, +// index, config, locks and git refs in this isolated machine. +function snapshot(dir = sandbox, ignoreDebugLog = false): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => { + const file = path.join(dir, entry.name); + const relative = path.relative(sandbox, file); + // Real command errors use the ordinary diagnostic log; it is not a learning write. + if (ignoreDebugLog && file === path.join(home, '.teamai', 'debug.log')) return []; + return entry.isDirectory() + ? [`${relative}/`, ...snapshot(file, ignoreDebugLog)] + : [`${relative}:${createHash('sha256').update(fs.readFileSync(file)).digest('hex')}`]; + }); +} + +beforeEach(() => { + sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-namespace-e2e-')); + home = path.join(sandbox, 'home'); + project = path.join(sandbox, 'project'); + origin = path.join(sandbox, 'origin.git'); + clone = path.join(project, '.teamai', 'team-repo'); + configPath = path.join(project, '.teamai', 'config.yaml'); + const seed = path.join(sandbox, 'seed'); + fs.mkdirSync(path.join(home, '.teamai'), { recursive: true }); + fs.mkdirSync(seed); + fs.mkdirSync(project); + env = { + ...process.env, + HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: path.join(home, '.config'), + GIT_CONFIG_GLOBAL: path.join(home, '.gitconfig'), GIT_CONFIG_NOSYSTEM: '1', + GIT_AUTHOR_NAME: 'member', GIT_AUTHOR_EMAIL: 'member@example.invalid', + GIT_COMMITTER_NAME: 'member', GIT_COMMITTER_EMAIL: 'member@example.invalid', + GIT_TERMINAL_PROMPT: '0', FORCE_COLOR: '0', + }; + writeYaml(path.join(seed, 'teamai.yaml'), { + team: 'namespace-fixture', repo: origin, provider: 'git', usageReport: false, + }); + writeYaml(path.join(seed, 'manifest', 'projects.yaml'), { + version: 1, + projects: [ + { id: 'svc-a', resources: { learnings: ['svc-a', 'payments'] } }, + { id: 'alpha', resources: { learnings: ['alpha-notes'] } }, + ], + }); + git(['init', '-q', '-b', 'main'], seed); + git(['add', '.'], seed); + git(['commit', '-qm', 'fixture'], seed); + git(['clone', '-q', '--bare', seed, origin]); + git(['clone', '-q', origin, clone]); + writeYaml(configPath, { + repo: { localPath: clone, remote: origin, kind: 'git' }, + username: 'member', scope: 'project', projectRoot: project, + updatePolicy: 'skip', additionalRoles: [], projects: ['svc-a'], enabledAgents: ['claude'], + }); + note = path.join(sandbox, 'note.md'); + fs.writeFileSync(note, '# Narwhal contract\n\nNarwhal payments require a stable retry key.\n'); +}); + +afterEach(() => fs.rmSync(sandbox, { recursive: true, force: true })); + +describe('contribute --namespace through the built CLI (#916)', () => { + it('publishes to the chosen namespace and immediately recalls the published file', () => { + const result = run(['contribute', '--file', note, '--title', 'narwhal', '--namespace', 'payments']); + expect(result.code, result.output).toBe(0); + expect(result.output).toContain('Contributed: learnings/payments/narwhal-'); + const files = git(['ls-tree', '-r', '--name-only', 'teamai-learnings'], origin).split('\n'); + expect(files.filter((f) => f.endsWith('.md'))).toEqual([expect.stringMatching(/^learnings\/payments\/narwhal-/)]); + const recalled = run(['recall', 'narwhal']); + expect(recalled.code, recalled.output).toBe(0); + const file = recalled.output.match(/^File: (.+)$/m)?.[1]; + expect(file, recalled.output).toBeDefined(); + expect(file).toContain(`${path.sep}payments${path.sep}`); + expect(fs.readFileSync(file!, 'utf8')).toContain('stable retry key'); + expect(file).not.toContain('pending-learnings'); + }); + + it.each([ + { projects: ['svc-a'], destination: 'learnings/ (shared root)', allowed: 'svc-a, payments', prefix: 'learnings/narwhal-' }, + { projects: ['alpha'], destination: 'learnings/alpha-notes/', allowed: 'alpha-notes', prefix: 'learnings/alpha-notes/narwhal-' }, + { projects: [], destination: 'learnings/ (shared root)', allowed: '(none)', prefix: 'learnings/narwhal-' }, + ])('preserves the default and lists its choices for $projects', ({ projects, destination, allowed, prefix }) => { + setProjects(projects); + const before = snapshot(); + const listed = run(['projects', 'list']); + expect(listed.code, listed.output).toBe(0); + expect(listed.output).toContain(`Contributes to: ${destination} --namespace accepts: ${allowed}`); + expect(snapshot()).toEqual(before); + const result = run(['contribute', '--file', note, '--title', 'narwhal']); + expect(result.code, result.output).toBe(0); + expect(result.output).toContain(`Contributed: ${prefix}`); + expect(git(['ls-tree', '-r', '--name-only', 'teamai-learnings'], origin)).toContain(prefix); + if (projects[0] === 'svc-a') expect(result.output).toContain('pass --namespace <ns>: svc-a, payments'); + }); + + it.each(['project', 'user'])('previews the selected %s namespace without filesystem writes', (scope) => { + if (scope === 'user') { + const config = YAML.parse(fs.readFileSync(configPath, 'utf8')); + writeYaml(path.join(home, '.teamai', 'config.yaml'), { ...config, scope: 'user', projectRoot: undefined }); + setProjects(['alpha']); // An explicit user scope must not use this project's choices. + } + const before = snapshot(); + const result = run(['contribute', '--scope', scope, '--file', note, '--namespace', 'payments', '--dry-run']); + expect(result.code, result.output).toBe(0); + expect(result.output).toContain('[dry-run] Would push: learnings/payments/'); + expect(snapshot()).toEqual(before); + }); + + it.each(['alpha-notes', '../escape', 'a/b', 'a\\b', ''])('rejects the unavailable namespace %j before writing', (namespace) => { + const before = snapshot(sandbox, true); + const result = run(['contribute', '--file', note, '--namespace', namespace]); + expect(result.code, result.output).toBe(1); + expect(result.output).toContain('Allowed learnings namespaces: svc-a, payments'); + expect(snapshot(sandbox, true)).toEqual(before); + }); + + it('keeps a team without a projects manifest at the shared root and rejects explicit namespaces', () => { + fs.rmSync(path.join(clone, 'manifest', 'projects.yaml')); + const before = snapshot(sandbox, true); + expect(run(['projects', 'list']).output).toContain('Contributes to: learnings/ (shared root) --namespace accepts: (none)'); + const result = run(['contribute', '--file', note, '--namespace', 'payments']); + expect(result.code, result.output).toBe(1); + expect(result.output).toContain('Allowed learnings namespaces: (none)'); + const preview = run(['contribute', '--file', note, '--title', 'narwhal', '--dry-run']); + expect(preview.code, preview.output).toBe(0); + expect(preview.output).toContain('Would push: learnings/narwhal-'); + expect(snapshot(sandbox, true)).toEqual(before); + }); + + it('retains the chosen namespace in the offline queue when a later contribution publishes it', () => { + fs.renameSync(origin, `${origin}.offline`); + const queued = run(['contribute', '--file', note, '--title', 'narwhal', '--namespace', 'payments']); + expect(queued.code, queued.output).toBe(0); + expect(queued.output).toContain('Saved locally'); + const pending = path.join(project, '.teamai', 'pending-learnings', 'payments'); + expect(fs.readdirSync(pending).some((file) => file.startsWith('narwhal-'))).toBe(true); + fs.renameSync(`${origin}.offline`, origin); + const retry = run(['contribute', '--file', note, '--title', 'shared']); + expect(retry.code, retry.output).toBe(0); + expect(retry.output).toContain('Contributed: learnings/shared-'); + const files = git(['ls-tree', '-r', '--name-only', 'teamai-learnings'], origin); + expect(files).toContain('learnings/payments/narwhal-'); + expect(files).toContain('learnings/shared-'); + }); +}); diff --git a/src/contribute.ts b/src/contribute.ts index 952b01193..107f743cb 100644 --- a/src/contribute.ts +++ b/src/contribute.ts @@ -24,9 +24,8 @@ import { getProjectSearchIndexPath, isSelfMode } from './types.js'; * - Zero (no project, or the active projects declare no learnings namespace) → * the shared root (empty string). * - Multiple active learnings namespaces → the shared root, because the - * contribution's ownership is ambiguous; a member on several projects can still - * target one explicitly by contributing from that project's directory. This - * favors the safe default (visible to all) over silently guessing a namespace. + * contribution's ownership is ambiguous. `contribute --namespace` lets the + * member choose one of these namespaces without changing this default. */ export async function resolveLearningsSubdir(localConfig: LocalConfig): Promise<string> { const namespaces = await resolveActiveLearningsNamespaces( @@ -146,11 +145,11 @@ export function generateFilename(title?: string): string { * dealt with. */ export async function contribute( - options: GlobalOptions & { file?: string; title?: string; sessionId?: string; scope?: string }, + options: GlobalOptions & { file?: string; title?: string; sessionId?: string; scope?: string; namespace?: string }, ): Promise<void> { // Validate file if (!options.file) { - log.error('Usage: teamai contribute --file <path> [--title <title>]'); + log.error('Usage: teamai contribute --file <path> [--title <title>] [--namespace <ns>]'); return; } @@ -188,9 +187,22 @@ export async function contribute( const username = localConfig.username; const filename = generateFilename(options.title); - // Route into an active-project subdir when there is exactly one, else the - // shared root. `relPath` is the learnings-relative path used everywhere. - const learningsSubdir = await resolveLearningsSubdir(localConfig); + const namespaces = await resolveActiveLearningsNamespaces( + localConfig.repo.localPath, + localConfig.projects ?? [], + ); + // Validate before saving the queue, creating worktrees or rebuilding indexes. + if (options.namespace !== undefined && + (!isSafeNamespaceSegment(options.namespace) || !namespaces.includes(options.namespace))) { + log.error(`Cannot contribute to namespace "${options.namespace}". Allowed learnings namespaces: ${namespaces.join(', ') || '(none)'}.`); + process.exitCode = 1; + return; + } + const learningsSubdir = options.namespace ?? (namespaces.length === 1 ? namespaces[0] : ''); + if (options.namespace === undefined && namespaces.length > 1) { + log.info(`Contributing to learnings/ (shared root). To choose a namespace, pass --namespace <ns>: ${namespaces.join(', ')}.`); + } + // Keep the chosen path in the queue so retries publish to the same namespace. const relPath = learningsSubdir ? path.posix.join(learningsSubdir, filename) : filename; if (options.dryRun) { diff --git a/src/index.ts b/src/index.ts index 0371f3bbc..19fd4acc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1081,9 +1081,10 @@ program program .command('contribute') - .description('Contribute session knowledge to team repo') + .description('Contribute session knowledge to team repo; projects list shows the default destination and allowed namespaces') .option('--file <path>', 'Path to the contribution document') .option('--title <title>', 'Title for the contribution document') + .option('--namespace <ns>', 'Write to an active learnings namespace (default: the only active namespace, or the shared root when none or several are active)') .option('--session-id <id>', 'Session ID for dedup tracking') .option('--scope <scope>', 'Target scope: user or project') .action(async (cmdOpts) => { diff --git a/src/projects-cmd.ts b/src/projects-cmd.ts index 1bc05055b..3f0646c0b 100644 --- a/src/projects-cmd.ts +++ b/src/projects-cmd.ts @@ -15,6 +15,7 @@ import { listProjectIds, unknownProjectMessage, PROJECT_RESOURCE_TYPES, + resolveActiveLearningsNamespaces, } from './projects.js'; import type { ProjectsManifest, TeamProject } from './projects.js'; import { pullLatest, runManifestEdit, pushManifestChange } from './manifest-edit.js'; @@ -48,9 +49,13 @@ export async function projectsList(_options: GlobalOptions): Promise<void> { const repoPath = localConfig.repo.localPath; const manifest = await loadProjectsManifest(repoPath); + const namespaces = await resolveActiveLearningsNamespaces(repoPath, localConfig.projects ?? []); + const destination = namespaces.length === 1 ? `learnings/${namespaces[0]}/` : 'learnings/ (shared root)'; + const contributionTarget = `Contributes to: ${destination} --namespace accepts: ${namespaces.join(', ') || '(none)'}`; if (!manifest) { log.info('This team repo defines no projects (no manifest/projects.yaml).'); log.info('Projects are optional — resources fall back to roles + shared learnings.'); + console.log(contributionTarget); return; } @@ -76,6 +81,7 @@ export async function projectsList(_options: GlobalOptions): Promise<void> { } else { console.log('No active projects in this directory. Run `teamai projects set <id>` to set them.'); } + console.log(contributionTarget); } // ─── projects set ─────────────────────────────────────── From 1328e4dde8afd79b8d9d3ec869b47d39c7f39bb9 Mon Sep 17 00:00:00 2001 From: wojiecihuo <91608029+zszz3@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:02:01 +0800 Subject: [PATCH 2/3] chore: revert skill changes from namespace proposal --- skill-data/core/references/commands.md | 3 +-- skill-data/share/SKILL.md | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/skill-data/core/references/commands.md b/skill-data/core/references/commands.md index 9b46ab27e..0cc5ed3eb 100644 --- a/skill-data/core/references/commands.md +++ b/skill-data/core/references/commands.md @@ -295,10 +295,9 @@ Generated: do not edit by hand. Regenerate with ## contribute -- `teamai contribute` — Contribute session knowledge to team repo; projects list shows the default destination and allowed namespaces +- `teamai contribute` — Contribute session knowledge to team repo - `--file <path>` — Path to the contribution document - `--title <title>` — Title for the contribution document - - `--namespace <ns>` — Write to an active learnings namespace (default: the only active namespace, or the shared root when none or several are active) - `--session-id <id>` — Session ID for dedup tracking - `--scope <scope>` — Target scope: user or project diff --git a/skill-data/share/SKILL.md b/skill-data/share/SKILL.md index fe54e33e2..12fb76c1f 100644 --- a/skill-data/share/SKILL.md +++ b/skill-data/share/SKILL.md @@ -30,9 +30,7 @@ URLs, paths and code identifiers stay as they are. - Which tools or skills proved especially useful - Pitfalls and things to watch out for 3. **Save it**: write the document to a temporary file -4. **Choose the destination**: in the directory you worked in, run `teamai projects list`. - Keep the displayed default or choose one of its accepted namespaces; see `teamai contribute --help` for options. -5. **Push it to the team**: run `teamai contribute --file <path> --title "<title>"` in that directory, using the destination you chose. +4. **Push it to the team**: run `teamai contribute --file <path> --title "<title>"` ## Document Template @@ -49,7 +47,7 @@ teamai contribute --file /tmp/session-summary.md --title "Debugging K8s pod star ## Important -- Run this as a **sub-agent** (Agent tool) to avoid polluting the main session's context; pass it the directory you worked in so it uses that directory's destinations +- Run this as a **sub-agent** (Agent tool) to avoid polluting the main session's context - The document is pushed to the team repo's `teamai-learnings` branch, under `learnings/`, with no pull request - Team members will see it on their next `teamai pull` - Keep summaries concise and actionable — this is a knowledge base, not a diary From ca5ecc64460001cd94c745a294e95a8bf4284ca5 Mon Sep 17 00:00:00 2001 From: wojiecihuo <91608029+zszz3@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:44:36 +0800 Subject: [PATCH 3/3] docs: sync generated contribute command reference --- skill-data/core/references/commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skill-data/core/references/commands.md b/skill-data/core/references/commands.md index 0cc5ed3eb..9b46ab27e 100644 --- a/skill-data/core/references/commands.md +++ b/skill-data/core/references/commands.md @@ -295,9 +295,10 @@ Generated: do not edit by hand. Regenerate with ## contribute -- `teamai contribute` — Contribute session knowledge to team repo +- `teamai contribute` — Contribute session knowledge to team repo; projects list shows the default destination and allowed namespaces - `--file <path>` — Path to the contribution document - `--title <title>` — Title for the contribution document + - `--namespace <ns>` — Write to an active learnings namespace (default: the only active namespace, or the shared root when none or several are active) - `--session-id <id>` — Session ID for dedup tracking - `--scope <scope>` — Target scope: user or project