teamai-cli 0.25.0-beta.0 → 0.25.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1344,7 +1344,8 @@ function resolveRoleResourceNamespaces(input) {
1344
1344
  skills: [],
1345
1345
  // Roles never contribute learnings namespaces; only projects do. Kept empty
1346
1346
  // so the shape matches project resolution for a clean union at the call site.
1347
- learnings: []
1347
+ learnings: [],
1348
+ agents: []
1348
1349
  };
1349
1350
  for (const type of ROLE_RESOURCE_TYPES) {
1350
1351
  const seen = /* @__PURE__ */ new Set();
@@ -1388,10 +1389,13 @@ var init_roles = __esm({
1388
1389
  "use strict";
1389
1390
  init_fs();
1390
1391
  init_logger();
1391
- ROLE_RESOURCE_TYPES = ["knowledge", "skills"];
1392
+ ROLE_RESOURCE_TYPES = ["knowledge", "skills", "agents"];
1392
1393
  RoleResourceNamespacesSchema = z2.object({
1393
1394
  knowledge: z2.array(z2.string().min(1)),
1394
1395
  skills: z2.array(z2.string().min(1)),
1396
+ // Optional: a role without `agents` receives root-level agents only, which
1397
+ // is what every manifest written before this key existed already got.
1398
+ agents: z2.array(z2.string().min(1)).default([]),
1395
1399
  // learnings is accepted for backward compatibility but ignored at runtime.
1396
1400
  // All learnings are shared flat across the entire team (no namespace isolation).
1397
1401
  learnings: z2.array(z2.string()).optional()
@@ -2626,6 +2630,7 @@ function toZcodeEntry(def) {
2626
2630
  PostToolUse: 3e4,
2627
2631
  UserPromptSubmit: 6e4
2628
2632
  };
2633
+ const timeoutMs = def.timeout !== void 0 ? def.timeout * 1e3 : ZCODE_TIMEOUT_MS[def.event] ?? 6e4;
2629
2634
  const entry = process.platform === "win32" ? {
2630
2635
  // Windows must NOT spawn bare `bash`: CreateProcess resolves it to
2631
2636
  // System32's WSL launcher before any PATH directory, and the WSL side
@@ -2635,7 +2640,7 @@ function toZcodeEntry(def) {
2635
2640
  type: "process",
2636
2641
  command: "cmd",
2637
2642
  args: ["/c", def.command],
2638
- timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 6e4
2643
+ timeoutMs
2639
2644
  } : {
2640
2645
  type: "process",
2641
2646
  command: "bash",
@@ -2643,7 +2648,7 @@ function toZcodeEntry(def) {
2643
2648
  // so managed-entry detection and the managed-hooks manifest share one
2644
2649
  // command representation (the same invariant the Codex format keeps).
2645
2650
  args: ["-lc", def.command],
2646
- timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 6e4
2651
+ timeoutMs
2647
2652
  };
2648
2653
  const group = { hooks: [entry] };
2649
2654
  if (def.matcher && def.matcher !== "*") group.matcher = def.matcher;
@@ -8058,20 +8063,73 @@ var init_dashboard_collector = __esm({
8058
8063
  }
8059
8064
  });
8060
8065
 
8066
+ // src/utils/exec.ts
8067
+ import spawn from "cross-spawn";
8068
+ async function probeBinary(binary, args = ["--version"], executor = execCommand) {
8069
+ try {
8070
+ const result = await executor(binary, args, { timeoutMs: 5e3 });
8071
+ if (result.code !== 0) return "";
8072
+ return (result.stdout || result.stderr).trim();
8073
+ } catch {
8074
+ return "";
8075
+ }
8076
+ }
8077
+ function formatCommand(command, args) {
8078
+ const quote = (value) => /^[a-zA-Z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value);
8079
+ return [command, ...args.map(quote)].join(" ");
8080
+ }
8081
+ var execCommand;
8082
+ var init_exec = __esm({
8083
+ "src/utils/exec.ts"() {
8084
+ "use strict";
8085
+ execCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
8086
+ const child = spawn(command, args, {
8087
+ cwd: options.cwd,
8088
+ env: options.env,
8089
+ stdio: ["ignore", "pipe", "pipe"],
8090
+ shell: false
8091
+ });
8092
+ let stdout = "";
8093
+ let stderr = "";
8094
+ let settled = false;
8095
+ const timeoutMs = options.timeoutMs ?? 12e4;
8096
+ const finish = (callback) => {
8097
+ if (settled) return;
8098
+ settled = true;
8099
+ clearTimeout(timer);
8100
+ callback();
8101
+ };
8102
+ child.stdout?.on("data", (chunk) => {
8103
+ const text = chunk.toString();
8104
+ stdout += text;
8105
+ if (options.stream) process.stdout.write(text);
8106
+ });
8107
+ child.stderr?.on("data", (chunk) => {
8108
+ const text = chunk.toString();
8109
+ stderr += text;
8110
+ if (options.stream) process.stderr.write(text);
8111
+ });
8112
+ const timer = setTimeout(() => {
8113
+ child.kill();
8114
+ finish(() => reject(new Error(
8115
+ `Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`
8116
+ )));
8117
+ }, timeoutMs);
8118
+ child.on("error", (error) => finish(() => reject(error)));
8119
+ child.on("close", (code) => finish(() => resolve({
8120
+ stdout,
8121
+ stderr,
8122
+ code: code ?? 1
8123
+ })));
8124
+ });
8125
+ }
8126
+ });
8127
+
8061
8128
  // src/agent-version.ts
8062
- import { execFile } from "child_process";
8063
8129
  import { readFile } from "fs/promises";
8064
8130
  import path22 from "path";
8065
8131
  async function execVersion(bin, args = ["--version"]) {
8066
- return new Promise((resolve) => {
8067
- execFile(bin, args, { timeout: 5e3 }, (err, stdout) => {
8068
- if (err) {
8069
- resolve("");
8070
- return;
8071
- }
8072
- resolve(stdout.trim());
8073
- });
8074
- });
8132
+ return probeBinary(bin, args);
8075
8133
  }
8076
8134
  async function readPlistVersion(appPath) {
8077
8135
  const plistPath = path22.join(appPath, "Contents", "Info.plist");
@@ -8145,6 +8203,7 @@ var init_agent_version = __esm({
8145
8203
  "src/agent-version.ts"() {
8146
8204
  "use strict";
8147
8205
  init_logger();
8206
+ init_exec();
8148
8207
  VERSION_CACHE = /* @__PURE__ */ new Map();
8149
8208
  CODEBUDDY_IDE_PATHS = [
8150
8209
  "/Applications/CodeBuddy.app",
@@ -9305,7 +9364,7 @@ __export(local_agent_exports, {
9305
9364
  import fs14 from "fs";
9306
9365
  import os7 from "os";
9307
9366
  import path28 from "path";
9308
- import { execFile as execFile2 } from "child_process";
9367
+ import { execFile } from "child_process";
9309
9368
  import { promisify } from "util";
9310
9369
  import fse4 from "fs-extra";
9311
9370
  function modelAgentKind(tool) {
@@ -11544,7 +11603,7 @@ var init_local_agent = __esm({
11544
11603
  init_types();
11545
11604
  init_home();
11546
11605
  init_git2();
11547
- execFileAsync = promisify(execFile2);
11606
+ execFileAsync = promisify(execFile);
11548
11607
  LOCAL_AGENT_DIR = "local-agent";
11549
11608
  CONFIG_FILE = "config.json";
11550
11609
  MANIFEST_FILE = "manifest.json";
@@ -12441,6 +12500,7 @@ var init_skills = __esm({
12441
12500
  await this.addTombstone(name, localConfig);
12442
12501
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12443
12502
  if (!toolPath.skills) continue;
12503
+ if (isAgentExcluded(localConfig, tool)) continue;
12444
12504
  let skillDir;
12445
12505
  if (tool === "openclaw") {
12446
12506
  const wsDir = await resolveOpenclawWorkspaceDir();
@@ -12697,6 +12757,7 @@ var init_rules = __esm({
12697
12757
  await this.addTombstone(name, localConfig);
12698
12758
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12699
12759
  if (!toolPath.rules) continue;
12760
+ if (isAgentExcluded(localConfig, tool)) continue;
12700
12761
  const extensions = /* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"]);
12701
12762
  for (const extension of extensions) {
12702
12763
  const filePath = path32.join(baseDir, toolPath.rules, `${name}${extension}`);
@@ -12738,6 +12799,7 @@ var init_rules = __esm({
12738
12799
  const baseDir = resolveBaseDir(localConfig);
12739
12800
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12740
12801
  if (!toolPath.rules) continue;
12802
+ if (isAgentExcluded(localConfig, tool)) continue;
12741
12803
  if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
12742
12804
  const destDir = path32.join(baseDir, toolPath.rules);
12743
12805
  if (!await pathExists(destDir)) continue;
@@ -12941,9 +13003,9 @@ var init_env = __esm({
12941
13003
  }
12942
13004
  const envYamlPath = path34.join(localConfig.repo.localPath, "env", "env.yaml");
12943
13005
  if (!await pathExists(envYamlPath)) return [];
12944
- const { execFile: execFile5 } = await import("child_process");
13006
+ const { execFile: execFile4 } = await import("child_process");
12945
13007
  const { promisify: promisify4 } = await import("util");
12946
- const execFileAsync4 = promisify4(execFile5);
13008
+ const execFileAsync4 = promisify4(execFile4);
12947
13009
  try {
12948
13010
  await execFileAsync4("git", ["diff", "--exit-code", "env/env.yaml"], {
12949
13011
  cwd: localConfig.repo.localPath
@@ -13111,6 +13173,12 @@ import path35 from "path";
13111
13173
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
13112
13174
  import matter3 from "gray-matter";
13113
13175
  import { stringify as stringifyToml, parse as parseToml } from "smol-toml";
13176
+ function agentStemFromFilename(filename) {
13177
+ for (const ext of AGENT_FILE_EXTENSIONS) {
13178
+ if (filename.endsWith(ext)) return filename.slice(0, -ext.length);
13179
+ }
13180
+ return null;
13181
+ }
13114
13182
  function agentFileExtensionForTool(tool) {
13115
13183
  switch (tool) {
13116
13184
  case "codex":
@@ -13528,7 +13596,7 @@ function renderForTool(spec, tool) {
13528
13596
  return renderForOpencode(spec);
13529
13597
  }
13530
13598
  }
13531
- var ALL_SUPPORTED_TOOLS, KIRO_SESSION_START_COMMAND, COMMON_CLAUDE_FIELDS, COMMON_CURSOR_FIELDS, COMMON_CODEX_FIELDS, COMMON_KIRO_FIELDS, COMMON_OPENCODE_FIELDS, MERGE_COMMON_FIELDS;
13599
+ var ALL_SUPPORTED_TOOLS, AGENT_FILE_EXTENSIONS, KIRO_SESSION_START_COMMAND, COMMON_CLAUDE_FIELDS, COMMON_CURSOR_FIELDS, COMMON_CODEX_FIELDS, COMMON_KIRO_FIELDS, COMMON_OPENCODE_FIELDS, MERGE_COMMON_FIELDS;
13532
13600
  var init_agent_format = __esm({
13533
13601
  "src/resources/agent-format.ts"() {
13534
13602
  "use strict";
@@ -13548,6 +13616,7 @@ var init_agent_format = __esm({
13548
13616
  "zcode",
13549
13617
  "opencode"
13550
13618
  ];
13619
+ AGENT_FILE_EXTENSIONS = [".md", ".toml", ".json"];
13551
13620
  KIRO_SESSION_START_COMMAND = getDispatchCommand("session-start", "kiro");
13552
13621
  COMMON_CLAUDE_FIELDS = /* @__PURE__ */ new Set(["name", "description", "model", "tools"]);
13553
13622
  COMMON_CURSOR_FIELDS = /* @__PURE__ */ new Set(["agent_id", "description", "model", "tools"]);
@@ -13585,8 +13654,7 @@ async function removeStaleAgentSiblings(targetAgentsDir, stem, targetExt) {
13585
13654
  return;
13586
13655
  }
13587
13656
  for (const file of files) {
13588
- const base = file.replace(/\.(md|toml|json)$/, "");
13589
- if (base !== stem) continue;
13657
+ if (agentStemFromFilename(file) !== stem) continue;
13590
13658
  if (file === `${stem}${targetExt}`) continue;
13591
13659
  try {
13592
13660
  await remove(path36.join(targetAgentsDir, file));
@@ -13669,10 +13737,267 @@ var init_builtin_agents = __esm({
13669
13737
  }
13670
13738
  });
13671
13739
 
13672
- // src/resources/agents.ts
13740
+ // src/projects.ts
13741
+ var projects_exports = {};
13742
+ __export(projects_exports, {
13743
+ describeProjects: () => describeProjects,
13744
+ findProject: () => findProject,
13745
+ isSafeNamespaceSegment: () => isSafeNamespaceSegment,
13746
+ listProjectIds: () => listProjectIds,
13747
+ loadProjectsManifest: () => loadProjectsManifest,
13748
+ mergeNamespaces: () => mergeNamespaces,
13749
+ resolveActiveLearningsNamespaces: () => resolveActiveLearningsNamespaces,
13750
+ resolveProjectResourceNamespaces: () => resolveProjectResourceNamespaces,
13751
+ saveProjectsManifest: () => saveProjectsManifest
13752
+ });
13673
13753
  import path37 from "path";
13754
+ import YAML8 from "yaml";
13755
+ import { z as z6 } from "zod";
13756
+ function isSafeNamespaceSegment(seg) {
13757
+ return SAFE_ID.test(seg) && seg !== "." && seg !== "..";
13758
+ }
13759
+ function validateManifestShape2(raw) {
13760
+ if (!raw || typeof raw !== "object") {
13761
+ throw new Error("Invalid projects manifest: expected an object");
13762
+ }
13763
+ const candidate = raw;
13764
+ const projects = candidate.projects;
13765
+ if (projects !== void 0 && !Array.isArray(projects)) {
13766
+ throw new Error("Invalid projects manifest: projects must be an array");
13767
+ }
13768
+ for (const project of projects ?? []) {
13769
+ if (!project || typeof project !== "object") {
13770
+ throw new Error("Invalid projects manifest: every project must be an object");
13771
+ }
13772
+ const resources = project.resources;
13773
+ if (resources !== void 0 && (typeof resources !== "object" || Array.isArray(resources))) {
13774
+ throw new Error(`Invalid projects manifest: project ${project.id ?? "<unknown>"} has invalid resources`);
13775
+ }
13776
+ if (resources) {
13777
+ const ALLOWED_RESOURCE_KEYS = new Set(PROJECT_RESOURCE_TYPES);
13778
+ for (const key of Object.keys(resources)) {
13779
+ if (!ALLOWED_RESOURCE_KEYS.has(key)) {
13780
+ throw new Error(`Invalid projects manifest: unknown resource type "${key}"`);
13781
+ }
13782
+ }
13783
+ }
13784
+ }
13785
+ const manifest = ProjectsManifestSchema.parse(raw);
13786
+ const ids = /* @__PURE__ */ new Set();
13787
+ for (const project of manifest.projects) {
13788
+ if (ids.has(project.id)) {
13789
+ throw new Error(`Invalid projects manifest: duplicate project id "${project.id}"`);
13790
+ }
13791
+ ids.add(project.id);
13792
+ }
13793
+ return manifest;
13794
+ }
13795
+ async function loadProjectsManifest(repoPath) {
13796
+ const manifestPath = path37.join(repoPath, "manifest", "projects.yaml");
13797
+ const content = await readFileSafe(manifestPath);
13798
+ if (!content) {
13799
+ return null;
13800
+ }
13801
+ let raw;
13802
+ try {
13803
+ raw = YAML8.parse(content);
13804
+ } catch (error) {
13805
+ throw new Error(`Invalid projects manifest YAML: ${error.message}`);
13806
+ }
13807
+ return validateManifestShape2(raw);
13808
+ }
13809
+ async function saveProjectsManifest(repoPath, manifest) {
13810
+ validateManifestShape2(manifest);
13811
+ const manifestDir = path37.join(repoPath, "manifest");
13812
+ const manifestPath = path37.join(manifestDir, "projects.yaml");
13813
+ await ensureDir(manifestDir);
13814
+ await writeFile(manifestPath, YAML8.stringify(manifest));
13815
+ }
13816
+ function findProject(manifest, projectId) {
13817
+ return manifest.projects.find((candidate) => candidate.id === projectId);
13818
+ }
13819
+ function listProjectIds(manifest) {
13820
+ return manifest.projects.map((project) => project.id);
13821
+ }
13822
+ function describeProjects(projects) {
13823
+ return projects.map((project) => {
13824
+ const label = project.name || project.id;
13825
+ return project.description ? `${label}: ${project.description}` : label;
13826
+ });
13827
+ }
13828
+ function getProjectOrThrow(manifest, projectId) {
13829
+ const project = findProject(manifest, projectId);
13830
+ if (!project) {
13831
+ throw new Error(`Unknown project "${projectId}". Valid projects: ${listProjectIds(manifest).join(", ")}`);
13832
+ }
13833
+ return project;
13834
+ }
13835
+ function resolveProjectResourceNamespaces(input) {
13836
+ const resolved = input.activeProjects.map((id) => getProjectOrThrow(input.manifest, id));
13837
+ const namespaces = {
13838
+ knowledge: [],
13839
+ skills: [],
13840
+ learnings: [],
13841
+ agents: []
13842
+ };
13843
+ for (const type of PROJECT_RESOURCE_TYPES) {
13844
+ const seen = /* @__PURE__ */ new Set();
13845
+ for (const project of resolved) {
13846
+ for (const namespace of project.resources[type]) {
13847
+ if (seen.has(namespace)) continue;
13848
+ seen.add(namespace);
13849
+ namespaces[type].push(namespace);
13850
+ }
13851
+ }
13852
+ }
13853
+ return namespaces;
13854
+ }
13855
+ async function resolveActiveLearningsNamespaces(repoPath, activeProjects) {
13856
+ if (activeProjects.length === 0) return [];
13857
+ const manifest = await loadProjectsManifest(repoPath);
13858
+ if (!manifest) return [];
13859
+ try {
13860
+ return resolveProjectResourceNamespaces({ manifest, activeProjects }).learnings;
13861
+ } catch {
13862
+ return [];
13863
+ }
13864
+ }
13865
+ function mergeNamespaces(roleNamespaces, projectNamespaces) {
13866
+ const dedupe = (a, b) => {
13867
+ const seen = /* @__PURE__ */ new Set();
13868
+ const out = [];
13869
+ for (const ns of [...a, ...b]) {
13870
+ if (seen.has(ns)) continue;
13871
+ seen.add(ns);
13872
+ out.push(ns);
13873
+ }
13874
+ return out;
13875
+ };
13876
+ return {
13877
+ knowledge: dedupe(roleNamespaces.knowledge, projectNamespaces.knowledge),
13878
+ skills: dedupe(roleNamespaces.skills, projectNamespaces.skills),
13879
+ // Roles never contribute learnings; this is effectively the project set.
13880
+ learnings: dedupe(roleNamespaces.learnings, projectNamespaces.learnings),
13881
+ agents: dedupe(roleNamespaces.agents, projectNamespaces.agents)
13882
+ };
13883
+ }
13884
+ var PROJECT_RESOURCE_TYPES, ProjectResourceNamespacesSchema, SAFE_ID, ProjectSchema, ProjectsManifestSchema;
13885
+ var init_projects = __esm({
13886
+ "src/projects.ts"() {
13887
+ "use strict";
13888
+ init_fs();
13889
+ PROJECT_RESOURCE_TYPES = ["knowledge", "skills", "learnings", "agents"];
13890
+ ProjectResourceNamespacesSchema = z6.object({
13891
+ knowledge: z6.array(z6.string().min(1)).default([]),
13892
+ skills: z6.array(z6.string().min(1)).default([]),
13893
+ learnings: z6.array(z6.string().min(1)).default([]),
13894
+ agents: z6.array(z6.string().min(1)).default([])
13895
+ });
13896
+ SAFE_ID = /^[A-Za-z0-9._-]+$/;
13897
+ ProjectSchema = z6.object({
13898
+ id: z6.string().min(1).refine((v) => isSafeNamespaceSegment(v), {
13899
+ message: "project id must be a single path segment (letters, digits, '.', '_', '-'; no '/', '\\\\', or '..')"
13900
+ }),
13901
+ name: z6.string().default(""),
13902
+ description: z6.string().default(""),
13903
+ resources: ProjectResourceNamespacesSchema
13904
+ });
13905
+ ProjectsManifestSchema = z6.object({
13906
+ version: z6.number(),
13907
+ // Unlike roles (`.min(1)`), a repo may define zero projects — a team without
13908
+ // project partitioning simply has no projects.yaml, and an empty list is valid.
13909
+ projects: z6.array(ProjectSchema).default([])
13910
+ });
13911
+ }
13912
+ });
13913
+
13914
+ // src/resource-namespaces.ts
13915
+ async function resolveResourceNamespaces(localConfig) {
13916
+ const activeProjects = localConfig.projects ?? [];
13917
+ const primaryRole = localConfig.primaryRole;
13918
+ const hasRole = !!primaryRole;
13919
+ const hasProjects = activeProjects.length > 0;
13920
+ const projectsManifest = await loadProjectsManifest(localConfig.repo.localPath);
13921
+ const teamHasProjects = !!projectsManifest && projectsManifest.projects.length > 0;
13922
+ if (!hasRole && !hasProjects && !teamHasProjects) return null;
13923
+ let roleNamespaces = { knowledge: [], skills: [], learnings: [], agents: [] };
13924
+ let allRoleSkillNamespaces = /* @__PURE__ */ new Set();
13925
+ if (primaryRole) {
13926
+ let rolesManifest;
13927
+ try {
13928
+ rolesManifest = await loadRolesManifest(localConfig.repo.localPath);
13929
+ } catch {
13930
+ log.warn("Could not load roles manifest. Skipping role-based filtering.");
13931
+ rolesManifest = null;
13932
+ }
13933
+ if (rolesManifest) {
13934
+ try {
13935
+ roleNamespaces = resolveRoleResourceNamespaces({
13936
+ manifest: rolesManifest,
13937
+ primaryRole,
13938
+ additionalRoles: localConfig.additionalRoles ?? []
13939
+ });
13940
+ allRoleSkillNamespaces = new Set(rolesManifest.roles.flatMap((role) => role.resources.skills));
13941
+ } catch {
13942
+ log.warn(`Role "${localConfig.primaryRole}" not found in manifest. Falling back to unfiltered sync.`);
13943
+ log.warn("Run `teamai roles set <role>` to pick a valid role.");
13944
+ if (!hasProjects && !teamHasProjects) return null;
13945
+ }
13946
+ } else if (!hasProjects && !teamHasProjects) {
13947
+ return null;
13948
+ }
13949
+ }
13950
+ let projectNamespaces = { knowledge: [], skills: [], learnings: [], agents: [] };
13951
+ let allProjectSkillNamespaces = /* @__PURE__ */ new Set();
13952
+ if (projectsManifest) {
13953
+ allProjectSkillNamespaces = new Set(projectsManifest.projects.flatMap((p) => p.resources.skills));
13954
+ if (hasProjects) {
13955
+ try {
13956
+ projectNamespaces = resolveProjectResourceNamespaces({
13957
+ manifest: projectsManifest,
13958
+ activeProjects
13959
+ });
13960
+ } catch (e) {
13961
+ log.warn(`${e instanceof Error ? e.message : String(e)} Falling back to role-only filtering.`);
13962
+ }
13963
+ }
13964
+ } else if (hasProjects) {
13965
+ log.warn("Active projects configured but no projects manifest found. Skipping project-based filtering.");
13966
+ }
13967
+ const activeNamespaces = mergeNamespaces(roleNamespaces, projectNamespaces);
13968
+ const allSkillNamespaces = /* @__PURE__ */ new Set([...allRoleSkillNamespaces, ...allProjectSkillNamespaces]);
13969
+ return { activeNamespaces, allSkillNamespaces };
13970
+ }
13971
+ var init_resource_namespaces = __esm({
13972
+ "src/resource-namespaces.ts"() {
13973
+ "use strict";
13974
+ init_roles();
13975
+ init_projects();
13976
+ init_logger();
13977
+ }
13978
+ });
13979
+
13980
+ // src/resources/agents.ts
13981
+ import path38 from "path";
13674
13982
  import { isDeepStrictEqual } from "util";
13675
13983
  import { parse as parseYaml2 } from "yaml";
13984
+ async function listTeamAgentDirs(teamAgentsDir) {
13985
+ const dirs = [{ dir: teamAgentsDir }];
13986
+ for (const namespace of await listDirs(teamAgentsDir)) {
13987
+ if (isSafeNamespaceSegment(namespace)) dirs.push({ dir: path38.join(teamAgentsDir, namespace), namespace });
13988
+ }
13989
+ return dirs;
13990
+ }
13991
+ async function findTeamAgentFiles(teamAgentsDir, stem) {
13992
+ const found = [];
13993
+ for (const { dir, namespace } of await listTeamAgentDirs(teamAgentsDir)) {
13994
+ for (const ext of [".yaml", ".md"]) {
13995
+ const candidate = path38.join(dir, `${stem}${ext}`);
13996
+ if (await pathExists(candidate)) found.push({ path: candidate, ext, ...namespace ? { namespace } : {} });
13997
+ }
13998
+ }
13999
+ return found;
14000
+ }
13676
14001
  function mergeCanonicalEdits(canonical, perTool) {
13677
14002
  const merged = { ...canonical };
13678
14003
  const extras = { ...canonical.tool_extras };
@@ -13714,16 +14039,10 @@ function mergeCanonicalEdits(canonical, perTool) {
13714
14039
  else delete merged.tool_extras;
13715
14040
  return { ok: true, spec: merged };
13716
14041
  }
13717
- function getAgentStem(filename) {
13718
- if (filename.endsWith(".md")) return filename.slice(0, -3);
13719
- if (filename.endsWith(".toml")) return filename.slice(0, -5);
13720
- if (filename.endsWith(".json")) return filename.slice(0, -5);
13721
- return null;
13722
- }
13723
14042
  async function removeStaleAgentSiblings2(agentsDir, stem, targetExt) {
13724
14043
  for (const file of await listFiles(agentsDir)) {
13725
- if (getAgentStem(file) !== stem || file === `${stem}${targetExt}`) continue;
13726
- await remove(path37.join(agentsDir, file));
14044
+ if (agentStemFromFilename(file) !== stem || file === `${stem}${targetExt}`) continue;
14045
+ await remove(path38.join(agentsDir, file));
13727
14046
  log.debug(`Removed stale agent sibling ${file} for ${stem}`);
13728
14047
  }
13729
14048
  }
@@ -13737,7 +14056,7 @@ async function agentContentEqual(tool, localPath, teamPath) {
13737
14056
  const canonicalContent = await readFileSafe(teamPath);
13738
14057
  const localContent = await readFileSafe(localPath);
13739
14058
  if (canonicalContent === null || localContent === null) return false;
13740
- const parsed = parseAgentYaml(canonicalContent, path37.basename(teamPath));
14059
+ const parsed = parseAgentYaml(canonicalContent, path38.basename(teamPath));
13741
14060
  if (!parsed.ok) return false;
13742
14061
  return localContent === renderForTool(parsed.spec, tool).content;
13743
14062
  }
@@ -13767,7 +14086,7 @@ function reverseByTool(tool, filePath, content) {
13767
14086
  return reverseFromOpencode(filePath, content);
13768
14087
  }
13769
14088
  }
13770
- var AgentsHandler;
14089
+ var AgentsHandler, LEGACY_MD_TOOLS;
13771
14090
  var init_agents = __esm({
13772
14091
  "src/resources/agents.ts"() {
13773
14092
  "use strict";
@@ -13776,6 +14095,9 @@ var init_agents = __esm({
13776
14095
  init_logger();
13777
14096
  init_types();
13778
14097
  init_builtin_agents();
14098
+ init_resource_namespaces();
14099
+ init_projects();
14100
+ init_path_safety();
13779
14101
  init_agent_format();
13780
14102
  AgentsHandler = class extends ResourceHandler {
13781
14103
  type = "agents";
@@ -13787,50 +14109,53 @@ var init_agents = __esm({
13787
14109
  * Built-in CLI agents are excluded from push.
13788
14110
  */
13789
14111
  async scanLocalForPush(teamConfig, localConfig) {
13790
- const teamAgentsDir = path37.join(localConfig.repo.localPath, "agents");
14112
+ const teamAgentsDir = path38.join(localConfig.repo.localPath, "agents");
13791
14113
  const tombstones = await this.readTombstones(localConfig);
13792
14114
  const baseDir = resolveBaseDir(localConfig);
13793
14115
  const directItems = [];
13794
14116
  const directStems = /* @__PURE__ */ new Set();
13795
14117
  if (isSelfMode(localConfig) && localConfig.projectRoot) {
13796
- const activeAgentsDir = path37.join(localConfig.projectRoot, ".teamai", "agents");
14118
+ const activeAgentsDir = path38.join(localConfig.projectRoot, ".teamai", "agents");
13797
14119
  if (await pathExists(activeAgentsDir)) {
13798
- for (const file of await listFiles(activeAgentsDir)) {
13799
- const isYaml = file.endsWith(".yaml");
13800
- const isMd = file.endsWith(".md");
13801
- if (!isYaml && !isMd) continue;
13802
- const stem = file.replace(/\.(yaml|md)$/, "");
13803
- if (tombstones.has(stem)) continue;
13804
- if (BUILTIN_AGENT_NAMES.has(stem)) continue;
13805
- const activePath = path37.join(activeAgentsDir, file);
13806
- const basePath = path37.join(teamAgentsDir, file);
13807
- const baseExists = await pathExists(basePath);
13808
- if (baseExists && await fileContentEqual(activePath, basePath)) continue;
13809
- directItems.push({
13810
- name: stem,
13811
- type: "agents",
13812
- sourcePath: activePath,
13813
- relativePath: `agents/${file}`,
13814
- status: baseExists ? "modified" : "new",
13815
- legacy: isMd
13816
- });
13817
- directStems.add(stem);
14120
+ for (const { dir, namespace } of await listTeamAgentDirs(activeAgentsDir)) {
14121
+ const relDir = namespace ? `agents/${namespace}` : "agents";
14122
+ for (const file of await listFiles(dir)) {
14123
+ const isYaml = file.endsWith(".yaml");
14124
+ const isMd = file.endsWith(".md");
14125
+ if (!isYaml && !isMd) continue;
14126
+ const stem = file.replace(/\.(yaml|md)$/, "");
14127
+ if (tombstones.has(stem)) continue;
14128
+ if (BUILTIN_AGENT_NAMES.has(stem)) continue;
14129
+ const activePath = path38.join(dir, file);
14130
+ const basePath = path38.join(localConfig.repo.localPath, relDir, file);
14131
+ const baseExists = await pathExists(basePath);
14132
+ if (baseExists && await fileContentEqual(activePath, basePath)) continue;
14133
+ directItems.push({
14134
+ name: stem,
14135
+ type: "agents",
14136
+ sourcePath: activePath,
14137
+ relativePath: `${relDir}/${file}`,
14138
+ status: baseExists ? "modified" : "new",
14139
+ legacy: isMd
14140
+ });
14141
+ directStems.add(stem);
14142
+ }
13818
14143
  }
13819
14144
  }
13820
14145
  }
13821
14146
  const grouped = /* @__PURE__ */ new Map();
13822
14147
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13823
14148
  if (!toolPath.agents) continue;
13824
- const agentsDir = path37.join(baseDir, toolPath.agents);
14149
+ const agentsDir = path38.join(baseDir, toolPath.agents);
13825
14150
  if (!await pathExists(agentsDir)) continue;
13826
14151
  const files = await listFiles(agentsDir);
13827
14152
  for (const file of files) {
13828
- const stem = getAgentStem(file);
14153
+ const stem = agentStemFromFilename(file);
13829
14154
  if (stem === null) continue;
13830
14155
  if (tombstones.has(stem)) continue;
13831
14156
  if (BUILTIN_AGENT_NAMES.has(stem)) continue;
13832
14157
  if (directStems.has(stem)) continue;
13833
- const filePath = path37.join(agentsDir, file);
14158
+ const filePath = path38.join(agentsDir, file);
13834
14159
  let toolGroup = grouped.get(stem);
13835
14160
  if (!toolGroup) {
13836
14161
  toolGroup = /* @__PURE__ */ new Map();
@@ -13842,11 +14167,41 @@ var init_agents = __esm({
13842
14167
  }
13843
14168
  }
13844
14169
  const items = [...directItems];
14170
+ const resolved = await resolveResourceNamespaces(localConfig);
14171
+ const activeNamespaces = resolved?.activeNamespaces.agents ?? null;
13845
14172
  for (const [stem, toolFiles] of grouped) {
13846
- const teamYamlPath = path37.join(teamAgentsDir, `${stem}.yaml`);
13847
- const teamMdPath = path37.join(teamAgentsDir, `${stem}.md`);
13848
- const hasTeamYaml = await pathExists(teamYamlPath);
13849
- const hasTeamMd = await pathExists(teamMdPath);
14173
+ const sources = await findTeamAgentFiles(teamAgentsDir, stem);
14174
+ const candidates = sources.filter(
14175
+ (file) => activeNamespaces === null || !file.namespace || activeNamespaces.includes(file.namespace)
14176
+ );
14177
+ if (candidates.length > 1) {
14178
+ items.push({
14179
+ name: stem,
14180
+ type: "agents",
14181
+ sourcePath: teamAgentsDir,
14182
+ relativePath: `agents/${stem}.yaml`,
14183
+ status: "modified",
14184
+ skipReason: `Ambiguous agent "${stem}": multiple active sources (${candidates.map((file) => file.path).join(", ")}). Give active agents unique names before pushing.`
14185
+ });
14186
+ continue;
14187
+ }
14188
+ if (sources.length > 0 && candidates.length === 0) {
14189
+ items.push({
14190
+ name: stem,
14191
+ type: "agents",
14192
+ sourcePath: teamAgentsDir,
14193
+ relativePath: `agents/${stem}.yaml`,
14194
+ status: "modified",
14195
+ skipReason: `Agent "${stem}" has no active source. Activate its role or project before pushing local edits.`
14196
+ });
14197
+ continue;
14198
+ }
14199
+ const located = candidates[0];
14200
+ const teamYamlPath = located?.ext === ".yaml" ? located.path : path38.join(teamAgentsDir, `${stem}.yaml`);
14201
+ const teamMdPath = located?.ext === ".md" ? located.path : path38.join(teamAgentsDir, `${stem}.md`);
14202
+ const hasTeamYaml = located?.ext === ".yaml";
14203
+ const hasTeamMd = located?.ext === ".md";
14204
+ const teamDir = located?.namespace ? `agents/${located.namespace}` : "agents";
13850
14205
  let canonicalSpec;
13851
14206
  if (hasTeamYaml) {
13852
14207
  const raw = await readFileSafe(teamYamlPath);
@@ -13856,7 +14211,7 @@ var init_agents = __esm({
13856
14211
  name: stem,
13857
14212
  type: "agents",
13858
14213
  sourcePath: teamYamlPath,
13859
- relativePath: `agents/${stem}.yaml`,
14214
+ relativePath: `${teamDir}/${stem}.yaml`,
13860
14215
  status: "modified",
13861
14216
  skipReason: "cannot read or parse canonical agent YAML"
13862
14217
  });
@@ -13934,7 +14289,7 @@ var init_agents = __esm({
13934
14289
  name: stem,
13935
14290
  type: "agents",
13936
14291
  sourcePath: bestPath,
13937
- relativePath: `agents/${stem}.yaml`,
14292
+ relativePath: `${teamDir}/${stem}.yaml`,
13938
14293
  status: status2,
13939
14294
  mergedSpec: mergeResult.spec
13940
14295
  });
@@ -13945,7 +14300,7 @@ var init_agents = __esm({
13945
14300
  name: stem,
13946
14301
  type: "agents",
13947
14302
  sourcePath: bestPath,
13948
- relativePath: `agents/${stem}.md`,
14303
+ relativePath: `${teamDir}/${stem}.md`,
13949
14304
  status: status2,
13950
14305
  skipReason
13951
14306
  });
@@ -13956,32 +14311,33 @@ var init_agents = __esm({
13956
14311
  * Scan team repo `agents/` for files to pull.
13957
14312
  * Recognizes both *.yaml (new) and *.md (legacy).
13958
14313
  * Hidden files (tombstones) are filtered out by listFiles.
14314
+ *
14315
+ * Root-level files are shared with everyone. One level of subdirectories
14316
+ * (`agents/<namespace>/`) carries role/project-scoped agents, the same
14317
+ * convention `rules/<namespace>/` uses; pull filters them by the active
14318
+ * `agents` namespaces. Deeper nesting is not scanned.
13959
14319
  */
13960
14320
  async scanTeamForPull(_teamConfig, localConfig) {
13961
- const agentsDir = path37.join(localConfig.repo.localPath, "agents");
14321
+ const agentsDir = path38.join(localConfig.repo.localPath, "agents");
13962
14322
  if (!await pathExists(agentsDir)) return [];
13963
- const files = await listFiles(agentsDir);
13964
14323
  const items = [];
13965
- for (const file of files) {
13966
- if (file.endsWith(".yaml")) {
13967
- const stem = file.replace(/\.yaml$/, "");
14324
+ const scanDir = async (dir, namespace) => {
14325
+ const prefix = namespace ? `agents/${namespace}` : "agents";
14326
+ for (const file of await listFiles(dir)) {
14327
+ const legacy = file.endsWith(".md");
14328
+ if (!legacy && !file.endsWith(".yaml")) continue;
13968
14329
  items.push({
13969
- name: stem,
14330
+ name: file.replace(/\.(yaml|md)$/, ""),
13970
14331
  type: "agents",
13971
- sourcePath: path37.join(agentsDir, file),
13972
- relativePath: `agents/${file}`,
13973
- legacy: false
13974
- });
13975
- } else if (file.endsWith(".md")) {
13976
- const stem = file.replace(/\.md$/, "");
13977
- items.push({
13978
- name: stem,
13979
- type: "agents",
13980
- sourcePath: path37.join(agentsDir, file),
13981
- relativePath: `agents/${file}`,
13982
- legacy: true
14332
+ sourcePath: path38.join(dir, file),
14333
+ relativePath: `${prefix}/${file}`,
14334
+ legacy,
14335
+ ...namespace ? { namespace } : {}
13983
14336
  });
13984
14337
  }
14338
+ };
14339
+ for (const { dir, namespace } of await listTeamAgentDirs(agentsDir)) {
14340
+ await scanDir(dir, namespace);
13985
14341
  }
13986
14342
  return items;
13987
14343
  }
@@ -13994,22 +14350,27 @@ var init_agents = __esm({
13994
14350
  async pushItem(item, _teamConfig, localConfig) {
13995
14351
  const agentItem = item;
13996
14352
  if (agentItem.skipReason) {
13997
- log.warn(`[agents] \u8DF3\u8FC7 ${item.name}: ${agentItem.skipReason}`);
13998
- log.warn(" \u5EFA\u8BAE\u4FEE\u6539\u540E\u91CD\u65B0 push \u8BE5 subagent");
14353
+ log.warn(`[agents] Skipped ${item.name}: ${agentItem.skipReason}`);
13999
14354
  return;
14000
14355
  }
14356
+ const teamDir = path38.resolve(localConfig.repo.localPath, path38.dirname(item.relativePath));
14357
+ assertWithinRoot(
14358
+ path38.join(localConfig.repo.localPath, "agents"),
14359
+ teamDir,
14360
+ `Invalid agent destination outside team repo agents directory: ${item.relativePath}`
14361
+ );
14001
14362
  if (agentItem.mergedSpec) {
14002
- const dest2 = path37.join(localConfig.repo.localPath, "agents", `${item.name}.yaml`);
14003
- await ensureDir(path37.dirname(dest2));
14363
+ const dest2 = path38.join(teamDir, `${item.name}.yaml`);
14364
+ await ensureDir(path38.dirname(dest2));
14004
14365
  const yamlContent = serializeAgentYaml(agentItem.mergedSpec);
14005
14366
  await writeFile(dest2, yamlContent);
14006
14367
  log.debug(`Wrote agent ${item.name} \u2192 team repo (YAML format)`);
14007
14368
  return;
14008
14369
  }
14009
14370
  const ext = item.sourcePath.endsWith(".yaml") ? ".yaml" : ".md";
14010
- const dest = path37.join(localConfig.repo.localPath, "agents", `${item.name}${ext}`);
14371
+ const dest = path38.join(teamDir, `${item.name}${ext}`);
14011
14372
  if (item.sourcePath !== dest) {
14012
- await ensureDir(path37.dirname(dest));
14373
+ await ensureDir(path38.dirname(dest));
14013
14374
  await copyFile(item.sourcePath, dest);
14014
14375
  }
14015
14376
  log.debug(`Copied agent ${item.name} \u2192 team repo (${ext} verbatim)`);
@@ -14053,12 +14414,12 @@ var init_agents = __esm({
14053
14414
  continue;
14054
14415
  }
14055
14416
  if (isAgentExcluded(localConfig, tool)) continue;
14056
- const destDir = path37.join(baseDir, toolPath.agents);
14417
+ const destDir = path38.join(baseDir, toolPath.agents);
14057
14418
  try {
14058
14419
  await ensureDir(destDir);
14059
14420
  const { ext, content: rendered } = renderForTool(spec, tool);
14060
14421
  await removeStaleAgentSiblings2(destDir, item.name, ext);
14061
- const dest = path37.join(destDir, `${item.name}${ext}`);
14422
+ const dest = path38.join(destDir, `${item.name}${ext}`);
14062
14423
  await writeFile(dest, rendered);
14063
14424
  log.debug(`Rendered agent ${item.name} \u2192 ${tool} (${ext})`);
14064
14425
  } catch (e) {
@@ -14074,19 +14435,17 @@ var init_agents = __esm({
14074
14435
  async removeItem(name, teamConfig, localConfig) {
14075
14436
  const removed = [];
14076
14437
  const baseDir = resolveBaseDir(localConfig);
14077
- const teamAgentsDir = path37.join(localConfig.repo.localPath, "agents");
14078
- for (const ext of [".yaml", ".md"]) {
14079
- const teamFile = path37.join(teamAgentsDir, `${name}${ext}`);
14080
- if (await pathExists(teamFile)) {
14081
- await remove(teamFile);
14082
- removed.push(teamFile);
14083
- }
14438
+ const teamAgentsDir = path38.join(localConfig.repo.localPath, "agents");
14439
+ for (const located of await findTeamAgentFiles(teamAgentsDir, name)) {
14440
+ await remove(located.path);
14441
+ removed.push(located.path);
14084
14442
  }
14085
14443
  await this.addTombstone(name, localConfig);
14086
14444
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14087
14445
  if (!toolPath.agents) continue;
14088
- for (const ext of [".md", ".toml", ".json"]) {
14089
- const filePath = path37.join(baseDir, toolPath.agents, `${name}${ext}`);
14446
+ if (isAgentExcluded(localConfig, tool)) continue;
14447
+ for (const ext of AGENT_FILE_EXTENSIONS) {
14448
+ const filePath = path38.join(baseDir, toolPath.agents, `${name}${ext}`);
14090
14449
  if (await pathExists(filePath)) {
14091
14450
  await remove(filePath);
14092
14451
  removed.push(filePath);
@@ -14096,14 +14455,69 @@ var init_agents = __esm({
14096
14455
  }
14097
14456
  return removed;
14098
14457
  }
14458
+ /**
14459
+ * Revocation pass for role/project scoping. Removes the deployed copies of
14460
+ * every agent whose namespace is no longer active, on every installed tool.
14461
+ *
14462
+ * Data-safety gate, same as inactive skills: a file is deleted only when it
14463
+ * is byte-equal to what pull would render from the team source. A local edit
14464
+ * is kept and reported so nothing unpushed is lost. Root-level agents and
14465
+ * agents still deployed to the same tool destination never qualify.
14466
+ */
14467
+ async cleanupInactiveNamespaces(teamConfig, localConfig, activeNamespaces) {
14468
+ const items = await this.scanTeamForPull(teamConfig, localConfig);
14469
+ const isActive = (item) => !item.namespace || activeNamespaces.includes(item.namespace);
14470
+ const active = items.filter(isActive);
14471
+ const inactive = items.filter((item) => !isActive(item) && !BUILTIN_AGENT_NAMES.has(item.name));
14472
+ if (inactive.length === 0) return;
14473
+ const baseDir = resolveBaseDir(localConfig);
14474
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14475
+ if (!toolPath.agents || !isKnownTool(tool) || isAgentExcluded(localConfig, tool)) continue;
14476
+ if (!await ResourceHandler.isToolInstalled(toolPath.agents, baseDir)) continue;
14477
+ const destDir = path38.join(baseDir, toolPath.agents);
14478
+ const activeDestinations = /* @__PURE__ */ new Set();
14479
+ for (const item of active) {
14480
+ const rendered = await this.renderedForTool(item, tool);
14481
+ if (rendered) activeDestinations.add(`${item.name}${rendered.ext}`);
14482
+ }
14483
+ for (const item of inactive) {
14484
+ const expected = await this.renderedForTool(item, tool);
14485
+ if (!expected || activeDestinations.has(`${item.name}${expected.ext}`)) continue;
14486
+ const deployed = path38.join(destDir, `${item.name}${expected.ext}`);
14487
+ const current = await readFileSafe(deployed);
14488
+ if (current === null) continue;
14489
+ if (current !== expected.content) {
14490
+ log.warn(`[${localConfig.scope}] Kept agent "${item.name}" (${tool}): it differs from the team source ${item.relativePath}. Back it up, then delete it manually.`);
14491
+ continue;
14492
+ }
14493
+ await remove(deployed);
14494
+ log.debug(`[${localConfig.scope}] Removed inactive role-scoped agent ${item.name} from ${tool}`);
14495
+ }
14496
+ }
14497
+ }
14498
+ /**
14499
+ * What `pullItem` writes for this agent on this tool, or null when the tool
14500
+ * is not a target (legacy `.md` only reaches LEGACY_MD_TOOLS, a YAML spec
14501
+ * honours `targets`, an unparsable spec is skipped like pull skips it).
14502
+ */
14503
+ async renderedForTool(item, tool) {
14504
+ const content = await readFileSafe(item.sourcePath);
14505
+ if (content === null) return null;
14506
+ if (item.legacy) {
14507
+ return LEGACY_MD_TOOLS.has(tool) ? { ext: ".md", content } : null;
14508
+ }
14509
+ const parsed = parseAgentYaml(content, `${item.name}.yaml`);
14510
+ if (!parsed.ok) return null;
14511
+ if (parsed.spec.targets && !parsed.spec.targets.includes(tool)) return null;
14512
+ return renderForTool(parsed.spec, tool);
14513
+ }
14099
14514
  // ─── Private helpers ──────────────────────────────────────────────────────
14100
14515
  /**
14101
14516
  * Legacy pull: copies .md as-is to Claude-compatible tools, including JoyCode.
14102
14517
  */
14103
14518
  async pullLegacyMd(item, teamConfig, baseDir, localConfig) {
14104
- const legacyTools = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
14105
14519
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14106
- if (!legacyTools.has(tool)) continue;
14520
+ if (!LEGACY_MD_TOOLS.has(tool)) continue;
14107
14521
  if (!toolPath.agents) {
14108
14522
  log.debug(`Skipping legacy agent sync for ${tool}: no agents path configured`);
14109
14523
  continue;
@@ -14113,10 +14527,10 @@ var init_agents = __esm({
14113
14527
  continue;
14114
14528
  }
14115
14529
  if (isAgentExcluded(localConfig, tool)) continue;
14116
- const destDir = path37.join(baseDir, toolPath.agents);
14530
+ const destDir = path38.join(baseDir, toolPath.agents);
14117
14531
  try {
14118
14532
  await ensureDir(destDir);
14119
- const dest = path37.join(destDir, `${item.name}.md`);
14533
+ const dest = path38.join(destDir, `${item.name}.md`);
14120
14534
  await copyFile(item.sourcePath, dest);
14121
14535
  log.debug(`Synced legacy agent ${item.name} \u2192 ${tool}`);
14122
14536
  } catch (e) {
@@ -14125,6 +14539,7 @@ var init_agents = __esm({
14125
14539
  }
14126
14540
  }
14127
14541
  };
14542
+ LEGACY_MD_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
14128
14543
  }
14129
14544
  });
14130
14545
 
@@ -14190,11 +14605,11 @@ __export(update_exports, {
14190
14605
  resolveRegistryForPackage: () => resolveRegistryForPackage,
14191
14606
  update: () => update
14192
14607
  });
14193
- import { execFile as execFile3 } from "child_process";
14608
+ import { execFile as execFile2 } from "child_process";
14194
14609
  import { promisify as promisify2 } from "util";
14195
14610
  import { randomUUID as randomUUID2 } from "crypto";
14196
14611
  import fs16 from "fs";
14197
- import path38 from "path";
14612
+ import path39 from "path";
14198
14613
  import { fileURLToPath as fileURLToPath4 } from "url";
14199
14614
  import fse7 from "fs-extra";
14200
14615
  function resolveRegistryForPackage(pkgName) {
@@ -14204,15 +14619,15 @@ function resolveRegistryForPackage(pkgName) {
14204
14619
  return PUBLIC_REGISTRY;
14205
14620
  }
14206
14621
  function resolveNpmCommand() {
14207
- const nodeDir = path38.dirname(process.execPath);
14622
+ const nodeDir = path39.dirname(process.execPath);
14208
14623
  const candidates = [
14209
14624
  // Bundled runtimes: npm installed flat next to node.exe.
14210
- path38.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
14625
+ path39.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
14211
14626
  // POSIX layout rooted at the node dir itself.
14212
- path38.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
14627
+ path39.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
14213
14628
  // Canonical POSIX install (official tarball, Homebrew, nvm): node lives in
14214
14629
  // <prefix>/bin with npm at <prefix>/lib/node_modules — one level up.
14215
- path38.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
14630
+ path39.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
14216
14631
  ];
14217
14632
  for (const c of candidates) {
14218
14633
  if (fs16.existsSync(c)) return { cmd: process.execPath, args: [c] };
@@ -14221,16 +14636,16 @@ function resolveNpmCommand() {
14221
14636
  return { cmd: "npm", args: [] };
14222
14637
  }
14223
14638
  function prefixFromEntryPath(entry, posix) {
14224
- const marker = `${path38.sep}node_modules${path38.sep}`;
14639
+ const marker = `${path39.sep}node_modules${path39.sep}`;
14225
14640
  const idx = entry.lastIndexOf(marker);
14226
14641
  if (idx <= 0) return null;
14227
14642
  const root = entry.slice(0, idx);
14228
14643
  const pkgDir = getCurrentPackageName();
14229
- if (posix && path38.basename(root) === "lib") {
14230
- const prefix = path38.dirname(root);
14231
- return fs16.existsSync(path38.join(prefix, "lib", "node_modules", pkgDir)) ? { prefix, global: true } : null;
14644
+ if (posix && path39.basename(root) === "lib") {
14645
+ const prefix = path39.dirname(root);
14646
+ return fs16.existsSync(path39.join(prefix, "lib", "node_modules", pkgDir)) ? { prefix, global: true } : null;
14232
14647
  }
14233
- return fs16.existsSync(path38.join(root, "node_modules", pkgDir)) ? { prefix: root, global: !posix } : null;
14648
+ return fs16.existsSync(path39.join(root, "node_modules", pkgDir)) ? { prefix: root, global: !posix } : null;
14234
14649
  }
14235
14650
  function resolveInstallPrefix() {
14236
14651
  return prefixFromEntryPath(fileURLToPath4(import.meta.url), process.platform !== "win32");
@@ -14355,7 +14770,7 @@ async function acquireLock(lockPath) {
14355
14770
  owner
14356
14771
  });
14357
14772
  try {
14358
- await ensureDir(path38.dirname(resolved));
14773
+ await ensureDir(path39.dirname(resolved));
14359
14774
  } catch {
14360
14775
  return false;
14361
14776
  }
@@ -14484,12 +14899,12 @@ async function doUpdate() {
14484
14899
  if (entry) {
14485
14900
  try {
14486
14901
  const installed = JSON.parse(fs16.readFileSync(
14487
- path38.join(path38.dirname(path38.dirname(entry)), "package.json"),
14902
+ path39.join(path39.dirname(path39.dirname(entry)), "package.json"),
14488
14903
  "utf-8"
14489
14904
  ));
14490
14905
  if (installed.version !== result.latest) {
14491
14906
  log.warn(
14492
- `The running install at ${path38.dirname(path38.dirname(entry))} is still v${installed.version ?? "unknown"} (expected v${result.latest}) \u2014 it may need a manual update.`
14907
+ `The running install at ${path39.dirname(path39.dirname(entry))} is still v${installed.version ?? "unknown"} (expected v${result.latest}) \u2014 it may need a manual update.`
14493
14908
  );
14494
14909
  }
14495
14910
  } catch {
@@ -14543,7 +14958,7 @@ var init_update = __esm({
14543
14958
  init_types();
14544
14959
  init_prompt();
14545
14960
  init_package_info();
14546
- execFileAsync2 = promisify2(execFile3);
14961
+ execFileAsync2 = promisify2(execFile2);
14547
14962
  PUBLIC_REGISTRY = "https://registry.npmjs.org";
14548
14963
  TNPM_REGISTRY = "http://r.tnpm.oa.com";
14549
14964
  VERSION_CHECK_TIMEOUT = 5e3;
@@ -14558,9 +14973,9 @@ var marketplace_exports = {};
14558
14973
  __export(marketplace_exports, {
14559
14974
  refreshMarketplace: () => refreshMarketplace
14560
14975
  });
14561
- import path39 from "path";
14976
+ import path40 from "path";
14562
14977
  async function extractSkillDescription2(skillDir) {
14563
- const skillMdPath = path39.join(skillDir, SKILL_MD2);
14978
+ const skillMdPath = path40.join(skillDir, SKILL_MD2);
14564
14979
  const content = await readFileSafe(skillMdPath);
14565
14980
  if (!content) return "";
14566
14981
  const { data } = parseFrontmatter(content);
@@ -14569,7 +14984,7 @@ async function extractSkillDescription2(skillDir) {
14569
14984
  return String(desc).replace(/\s+/g, " ").trim();
14570
14985
  }
14571
14986
  async function refreshMarketplace(repoPath) {
14572
- const marketplacePath = path39.join(repoPath, MARKETPLACE_PATH);
14987
+ const marketplacePath = path40.join(repoPath, MARKETPLACE_PATH);
14573
14988
  if (!await pathExists(marketplacePath)) {
14574
14989
  return false;
14575
14990
  }
@@ -14583,11 +14998,11 @@ async function refreshMarketplace(repoPath) {
14583
14998
  return false;
14584
14999
  }
14585
15000
  if (!Array.isArray(marketplace.plugins)) return false;
14586
- const skillsDir = path39.join(repoPath, "skills");
15001
+ const skillsDir = path40.join(repoPath, "skills");
14587
15002
  const currentSkills = /* @__PURE__ */ new Set();
14588
15003
  const dirs = await listDirs(skillsDir);
14589
15004
  for (const dir of dirs) {
14590
- const hasSkillMd = await pathExists(path39.join(skillsDir, dir, SKILL_MD2));
15005
+ const hasSkillMd = await pathExists(path40.join(skillsDir, dir, SKILL_MD2));
14591
15006
  if (hasSkillMd) {
14592
15007
  currentSkills.add(dir);
14593
15008
  }
@@ -14606,7 +15021,7 @@ async function refreshMarketplace(repoPath) {
14606
15021
  for (const skillName of currentSkills) {
14607
15022
  const existing = existingPlugins.get(skillName);
14608
15023
  if (existing) {
14609
- const desc = await extractSkillDescription2(path39.join(skillsDir, skillName));
15024
+ const desc = await extractSkillDescription2(path40.join(skillsDir, skillName));
14610
15025
  if (desc && desc !== existing.description) {
14611
15026
  existing.description = desc;
14612
15027
  changed = true;
@@ -14614,7 +15029,7 @@ async function refreshMarketplace(repoPath) {
14614
15029
  updatedPlugins.push(existing);
14615
15030
  existingPlugins.delete(skillName);
14616
15031
  } else {
14617
- const desc = await extractSkillDescription2(path39.join(skillsDir, skillName));
15032
+ const desc = await extractSkillDescription2(path40.join(skillsDir, skillName));
14618
15033
  updatedPlugins.push({
14619
15034
  name: skillName,
14620
15035
  source: `./skills/${skillName}`,
@@ -14651,177 +15066,6 @@ var init_marketplace = __esm({
14651
15066
  }
14652
15067
  });
14653
15068
 
14654
- // src/projects.ts
14655
- var projects_exports = {};
14656
- __export(projects_exports, {
14657
- describeProjects: () => describeProjects,
14658
- findProject: () => findProject,
14659
- isSafeNamespaceSegment: () => isSafeNamespaceSegment,
14660
- listProjectIds: () => listProjectIds,
14661
- loadProjectsManifest: () => loadProjectsManifest,
14662
- mergeNamespaces: () => mergeNamespaces,
14663
- resolveActiveLearningsNamespaces: () => resolveActiveLearningsNamespaces,
14664
- resolveProjectResourceNamespaces: () => resolveProjectResourceNamespaces,
14665
- saveProjectsManifest: () => saveProjectsManifest
14666
- });
14667
- import path40 from "path";
14668
- import YAML8 from "yaml";
14669
- import { z as z6 } from "zod";
14670
- function isSafeNamespaceSegment(seg) {
14671
- return SAFE_ID.test(seg) && seg !== "." && seg !== "..";
14672
- }
14673
- function validateManifestShape2(raw) {
14674
- if (!raw || typeof raw !== "object") {
14675
- throw new Error("Invalid projects manifest: expected an object");
14676
- }
14677
- const candidate = raw;
14678
- const projects = candidate.projects;
14679
- if (projects !== void 0 && !Array.isArray(projects)) {
14680
- throw new Error("Invalid projects manifest: projects must be an array");
14681
- }
14682
- for (const project of projects ?? []) {
14683
- if (!project || typeof project !== "object") {
14684
- throw new Error("Invalid projects manifest: every project must be an object");
14685
- }
14686
- const resources = project.resources;
14687
- if (resources !== void 0 && (typeof resources !== "object" || Array.isArray(resources))) {
14688
- throw new Error(`Invalid projects manifest: project ${project.id ?? "<unknown>"} has invalid resources`);
14689
- }
14690
- if (resources) {
14691
- const ALLOWED_RESOURCE_KEYS = new Set(PROJECT_RESOURCE_TYPES);
14692
- for (const key of Object.keys(resources)) {
14693
- if (!ALLOWED_RESOURCE_KEYS.has(key)) {
14694
- throw new Error(`Invalid projects manifest: unknown resource type "${key}"`);
14695
- }
14696
- }
14697
- }
14698
- }
14699
- const manifest = ProjectsManifestSchema.parse(raw);
14700
- const ids = /* @__PURE__ */ new Set();
14701
- for (const project of manifest.projects) {
14702
- if (ids.has(project.id)) {
14703
- throw new Error(`Invalid projects manifest: duplicate project id "${project.id}"`);
14704
- }
14705
- ids.add(project.id);
14706
- }
14707
- return manifest;
14708
- }
14709
- async function loadProjectsManifest(repoPath) {
14710
- const manifestPath = path40.join(repoPath, "manifest", "projects.yaml");
14711
- const content = await readFileSafe(manifestPath);
14712
- if (!content) {
14713
- return null;
14714
- }
14715
- let raw;
14716
- try {
14717
- raw = YAML8.parse(content);
14718
- } catch (error) {
14719
- throw new Error(`Invalid projects manifest YAML: ${error.message}`);
14720
- }
14721
- return validateManifestShape2(raw);
14722
- }
14723
- async function saveProjectsManifest(repoPath, manifest) {
14724
- validateManifestShape2(manifest);
14725
- const manifestDir = path40.join(repoPath, "manifest");
14726
- const manifestPath = path40.join(manifestDir, "projects.yaml");
14727
- await ensureDir(manifestDir);
14728
- await writeFile(manifestPath, YAML8.stringify(manifest));
14729
- }
14730
- function findProject(manifest, projectId) {
14731
- return manifest.projects.find((candidate) => candidate.id === projectId);
14732
- }
14733
- function listProjectIds(manifest) {
14734
- return manifest.projects.map((project) => project.id);
14735
- }
14736
- function describeProjects(projects) {
14737
- return projects.map((project) => {
14738
- const label = project.name || project.id;
14739
- return project.description ? `${label}: ${project.description}` : label;
14740
- });
14741
- }
14742
- function getProjectOrThrow(manifest, projectId) {
14743
- const project = findProject(manifest, projectId);
14744
- if (!project) {
14745
- throw new Error(`Unknown project "${projectId}". Valid projects: ${listProjectIds(manifest).join(", ")}`);
14746
- }
14747
- return project;
14748
- }
14749
- function resolveProjectResourceNamespaces(input) {
14750
- const resolved = input.activeProjects.map((id) => getProjectOrThrow(input.manifest, id));
14751
- const namespaces = {
14752
- knowledge: [],
14753
- skills: [],
14754
- learnings: []
14755
- };
14756
- for (const type of PROJECT_RESOURCE_TYPES) {
14757
- const seen = /* @__PURE__ */ new Set();
14758
- for (const project of resolved) {
14759
- for (const namespace of project.resources[type]) {
14760
- if (seen.has(namespace)) continue;
14761
- seen.add(namespace);
14762
- namespaces[type].push(namespace);
14763
- }
14764
- }
14765
- }
14766
- return namespaces;
14767
- }
14768
- async function resolveActiveLearningsNamespaces(repoPath, activeProjects) {
14769
- if (activeProjects.length === 0) return [];
14770
- const manifest = await loadProjectsManifest(repoPath);
14771
- if (!manifest) return [];
14772
- try {
14773
- return resolveProjectResourceNamespaces({ manifest, activeProjects }).learnings;
14774
- } catch {
14775
- return [];
14776
- }
14777
- }
14778
- function mergeNamespaces(roleNamespaces, projectNamespaces) {
14779
- const dedupe = (a, b) => {
14780
- const seen = /* @__PURE__ */ new Set();
14781
- const out = [];
14782
- for (const ns of [...a, ...b]) {
14783
- if (seen.has(ns)) continue;
14784
- seen.add(ns);
14785
- out.push(ns);
14786
- }
14787
- return out;
14788
- };
14789
- return {
14790
- knowledge: dedupe(roleNamespaces.knowledge, projectNamespaces.knowledge),
14791
- skills: dedupe(roleNamespaces.skills, projectNamespaces.skills),
14792
- // Roles never contribute learnings; this is effectively the project set.
14793
- learnings: dedupe(roleNamespaces.learnings, projectNamespaces.learnings)
14794
- };
14795
- }
14796
- var PROJECT_RESOURCE_TYPES, ProjectResourceNamespacesSchema, SAFE_ID, ProjectSchema, ProjectsManifestSchema;
14797
- var init_projects = __esm({
14798
- "src/projects.ts"() {
14799
- "use strict";
14800
- init_fs();
14801
- PROJECT_RESOURCE_TYPES = ["knowledge", "skills", "learnings"];
14802
- ProjectResourceNamespacesSchema = z6.object({
14803
- knowledge: z6.array(z6.string().min(1)).default([]),
14804
- skills: z6.array(z6.string().min(1)).default([]),
14805
- learnings: z6.array(z6.string().min(1)).default([])
14806
- });
14807
- SAFE_ID = /^[A-Za-z0-9._-]+$/;
14808
- ProjectSchema = z6.object({
14809
- id: z6.string().min(1).refine((v) => isSafeNamespaceSegment(v), {
14810
- message: "project id must be a single path segment (letters, digits, '.', '_', '-'; no '/', '\\\\', or '..')"
14811
- }),
14812
- name: z6.string().default(""),
14813
- description: z6.string().default(""),
14814
- resources: ProjectResourceNamespacesSchema
14815
- });
14816
- ProjectsManifestSchema = z6.object({
14817
- version: z6.number(),
14818
- // Unlike roles (`.min(1)`), a repo may define zero projects — a team without
14819
- // project partitioning simply has no projects.yaml, and an empty list is valid.
14820
- projects: z6.array(ProjectSchema).default([])
14821
- });
14822
- }
14823
- });
14824
-
14825
15069
  // src/pkg/types.ts
14826
15070
  import { z as z7 } from "zod";
14827
15071
  var PackageScopeSchema, NpmSpecSchema, ClaudeMarketplaceSchema, ClaudePluginSpecSchema, ClaudeEcosystemSchema, PackageSetSchema, PackageManifestSchema, NpmLockEntrySchema, ClaudeMarketplaceLockSchema, ClaudePluginLockSchema, PackageLockSchema;
@@ -15073,6 +15317,7 @@ __export(reports_branch_exports, {
15073
15317
  ensureReportsDir: () => ensureReportsDir,
15074
15318
  ensureReportsWorktree: () => ensureReportsWorktree,
15075
15319
  refreshReportsWorktree: () => refreshReportsWorktree,
15320
+ updateReports: () => updateReports,
15076
15321
  withKnowledgeWorktree: () => withKnowledgeWorktree
15077
15322
  });
15078
15323
  import path42 from "path";
@@ -15196,6 +15441,38 @@ async function writeWorktreeGitignore(wt) {
15196
15441
  ].join("\n");
15197
15442
  await writeFile(path42.join(wt, ".gitignore"), content);
15198
15443
  }
15444
+ async function commitAndPushReportsAt(wt, message, files, options = {}) {
15445
+ const git = createGit2(wt);
15446
+ await git.add(files);
15447
+ const status2 = await git.status();
15448
+ if (status2.staged.length === 0 && !options.pushIfUnchanged) {
15449
+ log.debug("[reports] nothing to commit");
15450
+ return false;
15451
+ }
15452
+ if (status2.staged.length > 0) await commitSkippingHooks(git, message);
15453
+ for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
15454
+ try {
15455
+ await git.push(["origin", REPORTS_BRANCH]);
15456
+ return true;
15457
+ } catch (pushErr) {
15458
+ if (attempt === MAX_PUSH_RETRIES) {
15459
+ log.debug(`[reports] push failed after ${attempt} attempts: ${pushErr.message}`);
15460
+ return false;
15461
+ }
15462
+ try {
15463
+ await git.fetch(["origin", REPORTS_BRANCH]);
15464
+ await git.rebase([`origin/${REPORTS_BRANCH}`]);
15465
+ } catch (rebaseErr) {
15466
+ log.debug(`[reports] rebase failed, retrying: ${rebaseErr.message}`);
15467
+ try {
15468
+ await git.rebase(["--abort"]);
15469
+ } catch {
15470
+ }
15471
+ }
15472
+ }
15473
+ }
15474
+ return false;
15475
+ }
15199
15476
  async function commitAndPushReports(localConfig, message, files, options = {}) {
15200
15477
  const lockPath = reportsLockPath(localConfig);
15201
15478
  const locked = await acquireLock(lockPath);
@@ -15205,38 +15482,37 @@ async function commitAndPushReports(localConfig, message, files, options = {}) {
15205
15482
  }
15206
15483
  try {
15207
15484
  const wt = await ensureReportsWorktree(localConfig);
15208
- const git = createGit2(wt);
15209
- await git.add(files);
15210
- const status2 = await git.status();
15211
- if (status2.staged.length === 0 && !options.pushIfUnchanged) {
15212
- log.debug("[reports] nothing to commit");
15213
- return false;
15485
+ return await commitAndPushReportsAt(wt, message, files, options);
15486
+ } catch (e) {
15487
+ log.debug(`[reports] commitAndPushReports failed (non-blocking): ${e.message}`);
15488
+ return false;
15489
+ } finally {
15490
+ await releaseLock(lockPath);
15491
+ }
15492
+ }
15493
+ async function updateReports(localConfig, write, options = {}) {
15494
+ if (!usesReportsBranch(localConfig)) {
15495
+ throw new Error("updateReports requires a repo that stores reports on the teamai-reports branch");
15496
+ }
15497
+ const lockPath = reportsLockPath(localConfig);
15498
+ if (!await acquireLock(lockPath)) {
15499
+ log.debug("[reports] another reports write is in progress; skipping");
15500
+ return false;
15501
+ }
15502
+ try {
15503
+ const wt = await ensureReportsWorktree(localConfig);
15504
+ try {
15505
+ await syncReportsWorktree(wt);
15506
+ } catch (e) {
15507
+ log.debug(`[reports] sync before write failed, writing onto the local copy: ${e.message}`);
15214
15508
  }
15215
- if (status2.staged.length > 0) await commitSkippingHooks(git, message);
15216
- for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
15217
- try {
15218
- await git.push(["origin", REPORTS_BRANCH]);
15219
- return true;
15220
- } catch (pushErr) {
15221
- if (attempt === MAX_PUSH_RETRIES) {
15222
- log.debug(`[reports] push failed after ${attempt} attempts: ${pushErr.message}`);
15223
- return false;
15224
- }
15225
- try {
15226
- await git.fetch(["origin", REPORTS_BRANCH]);
15227
- await git.rebase([`origin/${REPORTS_BRANCH}`]);
15228
- } catch (rebaseErr) {
15229
- log.debug(`[reports] rebase failed, retrying: ${rebaseErr.message}`);
15230
- try {
15231
- await git.rebase(["--abort"]);
15232
- } catch {
15233
- }
15234
- }
15235
- }
15509
+ const change = await write(wt);
15510
+ if (!change || change.files.length === 0) {
15511
+ return false;
15236
15512
  }
15237
- return false;
15513
+ return await commitAndPushReportsAt(wt, change.message, change.files, options);
15238
15514
  } catch (e) {
15239
- log.debug(`[reports] commitAndPushReports failed (non-blocking): ${e.message}`);
15515
+ log.debug(`[reports] updateReports failed (non-blocking): ${e.message}`);
15240
15516
  return false;
15241
15517
  } finally {
15242
15518
  await releaseLock(lockPath);
@@ -15706,6 +15982,7 @@ var init_known_agents = __esm({
15706
15982
  // src/init.ts
15707
15983
  var init_exports = {};
15708
15984
  __export(init_exports, {
15985
+ ALL_PROJECTS_SELECTOR: () => ALL_PROJECTS_SELECTOR,
15709
15986
  buildSelfModeGitignore: () => buildSelfModeGitignore,
15710
15987
  init: () => init,
15711
15988
  initHttp: () => initHttp,
@@ -15713,6 +15990,7 @@ __export(init_exports, {
15713
15990
  migrateSelfModeGitignore: () => migrateSelfModeGitignore,
15714
15991
  migrateSelfModeGitignoreContent: () => migrateSelfModeGitignoreContent,
15715
15992
  promptForSelfModeAgents: () => promptForSelfModeAgents,
15993
+ resolveActiveProjects: () => resolveActiveProjects,
15716
15994
  resolveInheritUserScope: () => resolveInheritUserScope,
15717
15995
  resolveInitRepo: () => resolveInitRepo,
15718
15996
  resolveInitScope: () => resolveInitScope,
@@ -15783,6 +16061,10 @@ async function promptForRoleProfile(repoPath, roleFlag) {
15783
16061
  resourceProfileVersion: manifest.version
15784
16062
  };
15785
16063
  }
16064
+ function dedupeIds(ids) {
16065
+ const seen = /* @__PURE__ */ new Set();
16066
+ return ids.filter((id) => seen.has(id) ? false : (seen.add(id), true));
16067
+ }
15786
16068
  async function resolveActiveProjects(repoPath, projectFlag) {
15787
16069
  const requested = (projectFlag ?? "").split(",").map((s) => s.trim()).filter(Boolean);
15788
16070
  if (requested.length === 0) {
@@ -15794,7 +16076,27 @@ async function resolveActiveProjects(repoPath, projectFlag) {
15794
16076
  `--project given but no projects manifest (manifest/projects.yaml) exists in the team repo.`
15795
16077
  );
15796
16078
  }
15797
- const validIds = new Set(listProjectIds(manifest));
16079
+ const declared = listProjectIds(manifest);
16080
+ if (requested.includes(ALL_PROJECTS_SELECTOR)) {
16081
+ if (requested.length > 1) {
16082
+ throw new Error(
16083
+ `--project "${ALL_PROJECTS_SELECTOR}" already covers every declared project; drop the other ids (got: ${requested.join(", ")}).`
16084
+ );
16085
+ }
16086
+ if (declared.length === 0) {
16087
+ log.warn(
16088
+ `--project "${ALL_PROJECTS_SELECTOR}" was given but manifest/projects.yaml declares no projects; nothing was activated.`
16089
+ );
16090
+ return { projects: [] };
16091
+ }
16092
+ if (declared.includes(ALL_PROJECTS_SELECTOR)) {
16093
+ log.warn(
16094
+ `manifest/projects.yaml declares a project with the id "${ALL_PROJECTS_SELECTOR}", which is the reserved --project selector: every project is activated (that one included). To activate only it, run \`teamai projects set ${ALL_PROJECTS_SELECTOR}\`.`
16095
+ );
16096
+ }
16097
+ return { projects: declared };
16098
+ }
16099
+ const validIds = new Set(declared);
15798
16100
  for (const id of requested) {
15799
16101
  if (!validIds.has(id)) {
15800
16102
  throw new Error(
@@ -15802,9 +16104,7 @@ async function resolveActiveProjects(repoPath, projectFlag) {
15802
16104
  );
15803
16105
  }
15804
16106
  }
15805
- const seen = /* @__PURE__ */ new Set();
15806
- const projects = requested.filter((id) => seen.has(id) ? false : (seen.add(id), true));
15807
- return { projects };
16107
+ return { projects: dedupeIds(requested) };
15808
16108
  }
15809
16109
  function resolveInitScope(rawScope, cwd, homeDir) {
15810
16110
  const cwdResolved = resolveRealPath(cwd);
@@ -16322,20 +16622,28 @@ async function initSelfRepo(options) {
16322
16622
  }
16323
16623
  if (!options.dryRun) {
16324
16624
  try {
16325
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16326
- const wt = await ensureReportsWorktree2(localConfig);
16327
- const memberDir = path45.join(wt, "members");
16328
- await ensureDir(memberDir);
16329
- const memberPath = path45.join(memberDir, `${username}.yaml`);
16330
- const isNewSelfMember = !await pathExists(memberPath);
16331
- const existingSelfMember = await getMemberConfig(wt, username);
16332
- const { config: selfMemberConfig, changed: selfMemberChanged } = mergeMemberConfig(existingSelfMember, {
16333
- username,
16334
- projects: localConfig.projects
16625
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16626
+ let isNewSelfMember = false;
16627
+ let selfMemberChanged = false;
16628
+ const pushed = await updateReports2(localConfig, async (wt) => {
16629
+ const memberDir = path45.join(wt, "members");
16630
+ await ensureDir(memberDir);
16631
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
16632
+ isNewSelfMember = !await pathExists(memberPath);
16633
+ const existingSelfMember = await getMemberConfig(wt, username);
16634
+ const merged = mergeMemberConfig(existingSelfMember, {
16635
+ username,
16636
+ projects: localConfig.projects
16637
+ });
16638
+ selfMemberChanged = merged.changed;
16639
+ if (!merged.changed) return null;
16640
+ await writeFile(memberPath, YAML11.stringify(merged.config));
16641
+ return {
16642
+ files: ["members/"],
16643
+ message: isNewSelfMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`
16644
+ };
16335
16645
  });
16336
16646
  if (selfMemberChanged) {
16337
- await writeFile(memberPath, YAML11.stringify(selfMemberConfig));
16338
- const pushed = await commitAndPushReports2(localConfig, isNewSelfMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`, ["members/"]);
16339
16647
  if (pushed) {
16340
16648
  log.success(isNewSelfMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
16341
16649
  } else {
@@ -16510,6 +16818,17 @@ async function init(options) {
16510
16818
  }
16511
16819
  } else {
16512
16820
  log.info(`Repo already exists at ${localPath}, using existing clone`);
16821
+ try {
16822
+ const pullResult = await pullRepoFastForward(localPath);
16823
+ if (pullResult !== "already up to date") {
16824
+ log.info(`Refreshed existing clone (${pullResult})`);
16825
+ }
16826
+ } catch (e) {
16827
+ log.error(
16828
+ `Failed to refresh existing clone at ${localPath}: ${e.message}. The local clone was left unchanged. Fix network/auth or resolve divergence (commit/stash local edits), or remove ${localPath} manually to start fresh.`
16829
+ );
16830
+ process.exit(1);
16831
+ }
16513
16832
  }
16514
16833
  } else {
16515
16834
  log.warn(`Existing ${localPath} is not a git repository, re-cloning`);
@@ -16656,29 +16975,37 @@ async function init(options) {
16656
16975
  let isNewMember = true;
16657
16976
  if (!options.dryRun) {
16658
16977
  try {
16659
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16660
- const wt = await ensureReportsWorktree2(reportsConfig);
16661
- const memberDir = path45.join(wt, "members");
16662
- await ensureDir(memberDir);
16663
- const memberPath = path45.join(memberDir, `${username}.yaml`);
16664
- isNewMember = !await pathExists(memberPath);
16665
- const existingMember = await getMemberConfig(wt, username);
16666
- const { config: memberConfig, changed: memberChanged } = mergeMemberConfig(existingMember, {
16667
- username,
16668
- projects: resolvedProjects
16978
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16979
+ let memberChanged = false;
16980
+ let memberProjects;
16981
+ const pushed = await updateReports2(reportsConfig, async (wt) => {
16982
+ const memberDir = path45.join(wt, "members");
16983
+ await ensureDir(memberDir);
16984
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
16985
+ isNewMember = !await pathExists(memberPath);
16986
+ const existingMember = await getMemberConfig(wt, username);
16987
+ const merged = mergeMemberConfig(existingMember, {
16988
+ username,
16989
+ projects: resolvedProjects
16990
+ });
16991
+ memberChanged = merged.changed;
16992
+ memberProjects = merged.config.projects;
16993
+ if (!merged.changed) return null;
16994
+ await writeFile(memberPath, YAML11.stringify(merged.config));
16995
+ return {
16996
+ files: ["members/"],
16997
+ message: isNewMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`
16998
+ };
16669
16999
  });
16670
17000
  if (memberChanged) {
16671
- await writeFile(memberPath, YAML11.stringify(memberConfig));
16672
- log.success(isNewMember ? `Registered as team member: ${username}` : `Updated member roster: ${username}${memberConfig.projects ? ` (projects: ${memberConfig.projects.join(", ")})` : ""}`);
16673
- const pushed = await commitAndPushReports2(reportsConfig, isNewMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`, ["members/"]);
17001
+ log.success(isNewMember ? `Registered as team member: ${username}` : `Updated member roster: ${username}${memberProjects ? ` (projects: ${memberProjects.join(", ")})` : ""}`);
16674
17002
  if (pushed) {
16675
17003
  log.success(isNewMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
16676
17004
  } else {
16677
17005
  log.warn("Member registration could not be pushed (no write access?). You are still set up locally.");
16678
17006
  }
16679
- } else {
17007
+ } else if (!isNewMember) {
16680
17008
  log.info(`Member ${username} already registered`);
16681
- isNewMember = false;
16682
17009
  }
16683
17010
  } catch (e) {
16684
17011
  log.warn(`Member registration skipped (non-blocking): ${e.message}`);
@@ -16804,6 +17131,7 @@ async function init(options) {
16804
17131
  log.info("Run `teamai status` to check current config.");
16805
17132
  closePrompt();
16806
17133
  }
17134
+ var ALL_PROJECTS_SELECTOR;
16807
17135
  var init_init = __esm({
16808
17136
  "src/init.ts"() {
16809
17137
  "use strict";
@@ -16822,6 +17150,7 @@ var init_init = __esm({
16822
17150
  init_members();
16823
17151
  init_prompt();
16824
17152
  init_known_agents();
17153
+ ALL_PROJECTS_SELECTOR = "all";
16825
17154
  }
16826
17155
  });
16827
17156
 
@@ -17144,13 +17473,19 @@ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConf
17144
17473
  }
17145
17474
  } : teamConfig;
17146
17475
  const pushableTypes = ["skills", "rules", "env", "agents"];
17147
- const allItems = [];
17476
+ const fullScan = [];
17148
17477
  for (const type of pushableTypes) {
17149
17478
  const handler = getHandler(type);
17150
17479
  const items = await handler.scanLocalForPush(scanTeamConfig, localConfig);
17151
- allItems.push(...items);
17480
+ fullScan.push(...items);
17152
17481
  }
17153
- const fullScan = [...allItems];
17482
+ const allItems = fullScan.filter((item) => {
17483
+ if (item.type === "agents" && "skipReason" in item && typeof item.skipReason === "string" && item.skipReason) {
17484
+ log.warn(`[agents] Skipped ${item.name}: ${item.skipReason}`);
17485
+ return false;
17486
+ }
17487
+ return true;
17488
+ });
17154
17489
  spin.stop();
17155
17490
  if (options.skill) {
17156
17491
  const skillBasename = path46.basename(
@@ -17536,6 +17871,7 @@ __export(git_exports, {
17536
17871
  listWorktrees: () => listWorktrees,
17537
17872
  normalizeRepoUrlForCompare: () => normalizeRepoUrlForCompare,
17538
17873
  pullRepo: () => pullRepo,
17874
+ pullRepoFastForward: () => pullRepoFastForward,
17539
17875
  pushLearningToOrigin: () => pushLearningToOrigin,
17540
17876
  pushRepoBranch: () => pushRepoBranch,
17541
17877
  pushRepoDirectly: () => pushRepoDirectly,
@@ -17674,6 +18010,14 @@ async function pullRepo(localPath) {
17674
18010
  return "reset to origin (diverged)";
17675
18011
  }
17676
18012
  }
18013
+ async function pullRepoFastForward(localPath) {
18014
+ const git = createGit2(localPath);
18015
+ const result = await git.pull(["--ff-only"]);
18016
+ if (result.summary.changes === 0 && result.summary.insertions === 0 && result.summary.deletions === 0) {
18017
+ return "already up to date";
18018
+ }
18019
+ return `${result.summary.changes} file(s) changed`;
18020
+ }
17677
18021
  async function getDefaultBranch(localPath) {
17678
18022
  const cached = defaultBranchCache.get(localPath);
17679
18023
  if (cached) return cached;
@@ -18249,19 +18593,19 @@ async function bootstrapSelfRepo(dir, opts) {
18249
18593
  log.debug(`[bootstrap] hook injection failed (non-blocking): ${e.message}`);
18250
18594
  }
18251
18595
  try {
18252
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
18253
- const wt = await ensureReportsWorktree2(localConfig);
18254
- const memberDir = path49.join(wt, "members");
18255
- await ensureDir(memberDir);
18256
- const memberPath = path49.join(memberDir, `${username}.yaml`);
18257
- if (!await pathExists(memberPath)) {
18596
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
18597
+ await updateReports2(localConfig, async (wt) => {
18598
+ const memberDir = path49.join(wt, "members");
18599
+ await ensureDir(memberDir);
18600
+ const memberPath = path49.join(memberDir, `${username}.yaml`);
18601
+ if (await pathExists(memberPath)) return null;
18258
18602
  await writeFile(memberPath, YAML14.stringify({
18259
18603
  username,
18260
18604
  displayName: username,
18261
18605
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
18262
18606
  }));
18263
- await commitAndPushReports2(localConfig, `[teamai] Register member: ${username}`, ["members/"]);
18264
- }
18607
+ return { files: ["members/"], message: `[teamai] Register member: ${username}` };
18608
+ });
18265
18609
  } catch (e) {
18266
18610
  log.debug(`[bootstrap] member registration skipped (non-blocking): ${e.message}`);
18267
18611
  }
@@ -18529,68 +18873,6 @@ var init_config = __esm({
18529
18873
  }
18530
18874
  });
18531
18875
 
18532
- // src/utils/exec.ts
18533
- import spawn from "cross-spawn";
18534
- async function probeBinary(binary, args = ["--version"], executor = execCommand) {
18535
- try {
18536
- const result = await executor(binary, args, { timeoutMs: 5e3 });
18537
- if (result.code !== 0) return "";
18538
- return (result.stdout || result.stderr).trim();
18539
- } catch {
18540
- return "";
18541
- }
18542
- }
18543
- function formatCommand(command, args) {
18544
- const quote = (value) => /^[a-zA-Z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value);
18545
- return [command, ...args.map(quote)].join(" ");
18546
- }
18547
- var execCommand;
18548
- var init_exec = __esm({
18549
- "src/utils/exec.ts"() {
18550
- "use strict";
18551
- execCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
18552
- const child = spawn(command, args, {
18553
- cwd: options.cwd,
18554
- env: options.env,
18555
- stdio: ["ignore", "pipe", "pipe"],
18556
- shell: false
18557
- });
18558
- let stdout = "";
18559
- let stderr = "";
18560
- let settled = false;
18561
- const timeoutMs = options.timeoutMs ?? 12e4;
18562
- const finish = (callback) => {
18563
- if (settled) return;
18564
- settled = true;
18565
- clearTimeout(timer);
18566
- callback();
18567
- };
18568
- child.stdout?.on("data", (chunk) => {
18569
- const text = chunk.toString();
18570
- stdout += text;
18571
- if (options.stream) process.stdout.write(text);
18572
- });
18573
- child.stderr?.on("data", (chunk) => {
18574
- const text = chunk.toString();
18575
- stderr += text;
18576
- if (options.stream) process.stderr.write(text);
18577
- });
18578
- const timer = setTimeout(() => {
18579
- child.kill();
18580
- finish(() => reject(new Error(
18581
- `Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`
18582
- )));
18583
- }, timeoutMs);
18584
- child.on("error", (error) => finish(() => reject(error)));
18585
- child.on("close", (code) => finish(() => resolve({
18586
- stdout,
18587
- stderr,
18588
- code: code ?? 1
18589
- })));
18590
- });
18591
- }
18592
- });
18593
-
18594
18876
  // src/pkg/adapters/base.ts
18595
18877
  var PackageAdapter;
18596
18878
  var init_base2 = __esm({
@@ -19596,9 +19878,9 @@ async function verifyStaging(legacyDir, staging) {
19596
19878
  throw new Error("migration verify: team-repo/.git missing after copy (clone would be broken)");
19597
19879
  }
19598
19880
  try {
19599
- const { execFile: execFile5 } = await import("child_process");
19881
+ const { execFile: execFile4 } = await import("child_process");
19600
19882
  const { promisify: promisify4 } = await import("util");
19601
- await promisify4(execFile5)("git", ["rev-parse", "HEAD"], { cwd: stagedRepo });
19883
+ await promisify4(execFile4)("git", ["rev-parse", "HEAD"], { cwd: stagedRepo });
19602
19884
  } catch (e) {
19603
19885
  throw new Error(
19604
19886
  `migration verify: the staged team-repo clone is not a usable git repository (${e.message})`
@@ -19983,6 +20265,7 @@ var init_tokenizer = __esm({
19983
20265
  // src/votes.ts
19984
20266
  var votes_exports = {};
19985
20267
  __export(votes_exports, {
20268
+ hasPendingVoteDeltas: () => hasPendingVoteDeltas,
19986
20269
  incrementRecalled: () => incrementRecalled,
19987
20270
  incrementUpvoted: () => incrementUpvoted,
19988
20271
  loadUserVotes: () => loadUserVotes,
@@ -20097,6 +20380,10 @@ function mergeDeltas(local, remote) {
20097
20380
  }
20098
20381
  return { version: 2, votes, deltas: {} };
20099
20382
  }
20383
+ async function hasPendingVoteDeltas(localVotesDir, username) {
20384
+ const local = await loadUserVotes(path57.join(localVotesDir, `${username}.yaml`));
20385
+ return Object.keys(local.deltas).length > 0;
20386
+ }
20100
20387
  async function syncVotesToTeam(repoPath, username, localVotesDir) {
20101
20388
  const localVotePath = path57.join(localVotesDir, `${username}.yaml`);
20102
20389
  const remoteVotePath = path57.join(repoPath, "votes", `${username}.yaml`);
@@ -22537,10 +22824,67 @@ async function reportUsageToTeam(repoPath, username, options) {
22537
22824
  const hasInterventions = hasInterventionDelta(interventionDelta);
22538
22825
  const hasPromptTokens = hasPromptTokenDelta(promptTokenDelta);
22539
22826
  const hasDaily = hasDailyDelta(dailyDelta);
22540
- let writeRoot = repoPath;
22827
+ const hasStats = hasUsage || hasInterventions || hasPromptTokens || hasDaily;
22828
+ const commitMsg = hasUsage ? `[teamai] Update usage stats for ${username}` : hasInterventions || hasPromptTokens || hasDaily ? `[teamai] Update session stats for ${username}` : `[teamai] Update votes for ${username}`;
22829
+ const writeReportFiles = async (writeRoot) => {
22830
+ if (hasStats) {
22831
+ const statsDir = path64.join(writeRoot, "stats");
22832
+ await ensureDir(statsDir);
22833
+ const statsPath = path64.join(statsDir, `${username}.yaml`);
22834
+ const existing = await readExistingStats(statsPath);
22835
+ if (useReportsBranch) {
22836
+ const previousContent = await readFileSafe(statsPath);
22837
+ restoreStats = () => writeFile(statsPath, previousContent ?? "");
22838
+ }
22839
+ const newStats = hasUsage ? aggregateUsage(events) : [];
22840
+ const merged = mergeStats(existing, username, newStats);
22841
+ if (hasInterventions) {
22842
+ merged.interventions = mergeInterventionStats(existing?.interventions, interventionDelta);
22843
+ }
22844
+ if (hasPromptTokens) {
22845
+ const pt = mergePromptTokenStats(existing?.prompts, existing?.tokens, promptTokenDelta);
22846
+ merged.prompts = pt.prompts;
22847
+ merged.tokens = pt.tokens;
22848
+ }
22849
+ if (hasDaily) {
22850
+ merged.daily = mergeDailyStats(existing?.daily, dailyDelta);
22851
+ }
22852
+ await writeFile(statsPath, YAML21.stringify(merged));
22853
+ filesToPush.push(`stats/${username}.yaml`);
22854
+ }
22855
+ try {
22856
+ if (await pathExists(getUserVotesDir())) {
22857
+ const { syncVotesToTeam: syncVotesToTeam2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
22858
+ const synced = await syncVotesToTeam2(writeRoot, username, getUserVotesDir());
22859
+ if (synced) {
22860
+ filesToPush.push(`votes/${username}.yaml`);
22861
+ }
22862
+ }
22863
+ } catch (e) {
22864
+ log.error(`Vote staging skipped: ${e.message}`);
22865
+ }
22866
+ };
22541
22867
  if (useReportsBranch && reportsConfig) {
22542
- const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22543
- writeRoot = await ensureReportsWorktree2(reportsConfig);
22868
+ let hasVotes = false;
22869
+ if (!hasStats && await pathExists(getUserVotesDir())) {
22870
+ const { hasPendingVoteDeltas: hasPendingVoteDeltas2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
22871
+ hasVotes = await hasPendingVoteDeltas2(getUserVotesDir(), username);
22872
+ }
22873
+ if (!hasStats && !hasVotes) {
22874
+ log.debug("No usage events or votes to report");
22875
+ return true;
22876
+ }
22877
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22878
+ const pushed = await updateReports2(reportsConfig, async (wt) => {
22879
+ filesToPush.length = 0;
22880
+ await writeReportFiles(wt);
22881
+ return filesToPush.length > 0 ? { files: [...filesToPush], message: commitMsg } : null;
22882
+ }, { pushIfUnchanged: true });
22883
+ if (!pushed) {
22884
+ log.debug("Auto-report push was not confirmed; keeping local report data");
22885
+ await restoreStats?.();
22886
+ return false;
22887
+ }
22544
22888
  } else {
22545
22889
  const git = createGit2(repoPath);
22546
22890
  if (!await isDedicatedRepoRoot(repoPath)) {
@@ -22564,56 +22908,11 @@ async function reportUsageToTeam(repoPath, username, options) {
22564
22908
  await writeFile(yamlPath, pendingTeamConfig);
22565
22909
  }
22566
22910
  }
22567
- }
22568
- if (hasUsage || hasInterventions || hasPromptTokens || hasDaily) {
22569
- const statsDir = path64.join(writeRoot, "stats");
22570
- await ensureDir(statsDir);
22571
- const statsPath = path64.join(statsDir, `${username}.yaml`);
22572
- const existing = await readExistingStats(statsPath);
22573
- if (useReportsBranch) {
22574
- const previousContent = await readFileSafe(statsPath);
22575
- restoreStats = () => writeFile(statsPath, previousContent ?? "");
22576
- }
22577
- const newStats = hasUsage ? aggregateUsage(events) : [];
22578
- const merged = mergeStats(existing, username, newStats);
22579
- if (hasInterventions) {
22580
- merged.interventions = mergeInterventionStats(existing?.interventions, interventionDelta);
22581
- }
22582
- if (hasPromptTokens) {
22583
- const pt = mergePromptTokenStats(existing?.prompts, existing?.tokens, promptTokenDelta);
22584
- merged.prompts = pt.prompts;
22585
- merged.tokens = pt.tokens;
22586
- }
22587
- if (hasDaily) {
22588
- merged.daily = mergeDailyStats(existing?.daily, dailyDelta);
22589
- }
22590
- await writeFile(statsPath, YAML21.stringify(merged));
22591
- filesToPush.push(`stats/${username}.yaml`);
22592
- }
22593
- try {
22594
- if (await pathExists(getUserVotesDir())) {
22595
- const { syncVotesToTeam: syncVotesToTeam2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
22596
- const synced = await syncVotesToTeam2(writeRoot, username, getUserVotesDir());
22597
- if (synced) {
22598
- filesToPush.push(`votes/${username}.yaml`);
22599
- }
22600
- }
22601
- } catch (e) {
22602
- log.error(`Vote staging skipped: ${e.message}`);
22603
- }
22604
- if (filesToPush.length === 0) {
22605
- log.debug("No usage events or votes to report");
22606
- return true;
22607
- }
22608
- const commitMsg = hasUsage ? `[teamai] Update usage stats for ${username}` : hasInterventions || hasPromptTokens || hasDaily ? `[teamai] Update session stats for ${username}` : `[teamai] Update votes for ${username}`;
22609
- if (useReportsBranch && reportsConfig) {
22610
- const { commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22611
- if (!await commitAndPushReports2(reportsConfig, commitMsg, filesToPush, { pushIfUnchanged: true })) {
22612
- log.debug("Auto-report push was not confirmed; keeping local report data");
22613
- await restoreStats?.();
22614
- return false;
22911
+ await writeReportFiles(repoPath);
22912
+ if (filesToPush.length === 0) {
22913
+ log.debug("No usage events or votes to report");
22914
+ return true;
22615
22915
  }
22616
- } else {
22617
22916
  await pushRepoDirectly(repoPath, commitMsg, filesToPush);
22618
22917
  }
22619
22918
  restoreStats = void 0;
@@ -22836,6 +23135,7 @@ __export(pull_exports, {
22836
23135
  compileClaudemd: () => compileClaudemd,
22837
23136
  compileCulture: () => compileCulture,
22838
23137
  compileRecallRulesBlock: () => compileRecallRulesBlock,
23138
+ filterAgentsByNamespaces: () => filterAgentsByNamespaces,
22839
23139
  filterRulesByKnowledgeNamespaces: () => filterRulesByKnowledgeNamespaces,
22840
23140
  injectRecallBlockIntoTools: () => injectRecallBlockIntoTools,
22841
23141
  pull: () => pull,
@@ -22913,58 +23213,9 @@ async function usageReportDisabled(repoPath) {
22913
23213
  return (await loadTeamConfig(repoPath))?.usageReport === false;
22914
23214
  }
22915
23215
  async function buildRolePullContext(localConfig) {
22916
- const activeProjects = localConfig.projects ?? [];
22917
- const hasRole = !!localConfig.primaryRole;
22918
- const hasProjects = activeProjects.length > 0;
22919
- const projectsManifest = await loadProjectsManifest(localConfig.repo.localPath);
22920
- const teamHasProjects = !!projectsManifest && projectsManifest.projects.length > 0;
22921
- if (!hasRole && !hasProjects && !teamHasProjects) return null;
22922
- let roleNamespaces = { knowledge: [], skills: [], learnings: [] };
22923
- let allRoleSkillNamespaces = /* @__PURE__ */ new Set();
22924
- if (hasRole) {
22925
- let rolesManifest;
22926
- try {
22927
- rolesManifest = await loadRolesManifest(localConfig.repo.localPath);
22928
- } catch {
22929
- log.warn("Could not load roles manifest. Skipping role-based filtering.");
22930
- rolesManifest = null;
22931
- }
22932
- if (rolesManifest) {
22933
- try {
22934
- roleNamespaces = resolveRoleResourceNamespaces({
22935
- manifest: rolesManifest,
22936
- primaryRole: localConfig.primaryRole,
22937
- additionalRoles: localConfig.additionalRoles ?? []
22938
- });
22939
- allRoleSkillNamespaces = new Set(rolesManifest.roles.flatMap((role) => role.resources.skills));
22940
- } catch {
22941
- log.warn(`Role "${localConfig.primaryRole}" not found in manifest. Falling back to unfiltered sync.`);
22942
- log.warn("Run `teamai roles set <role>` to pick a valid role.");
22943
- if (!hasProjects && !teamHasProjects) return null;
22944
- }
22945
- } else if (!hasProjects && !teamHasProjects) {
22946
- return null;
22947
- }
22948
- }
22949
- let projectNamespaces = { knowledge: [], skills: [], learnings: [] };
22950
- let allProjectSkillNamespaces = /* @__PURE__ */ new Set();
22951
- if (projectsManifest) {
22952
- allProjectSkillNamespaces = new Set(projectsManifest.projects.flatMap((p) => p.resources.skills));
22953
- if (hasProjects) {
22954
- try {
22955
- projectNamespaces = resolveProjectResourceNamespaces({
22956
- manifest: projectsManifest,
22957
- activeProjects
22958
- });
22959
- } catch (e) {
22960
- log.warn(`${e.message} Falling back to role-only filtering.`);
22961
- }
22962
- }
22963
- } else if (hasProjects) {
22964
- log.warn("Active projects configured but no projects manifest found. Skipping project-based filtering.");
22965
- }
22966
- const activeNamespaces = mergeNamespaces(roleNamespaces, projectNamespaces);
22967
- const allSkillNamespaces = /* @__PURE__ */ new Set([...allRoleSkillNamespaces, ...allProjectSkillNamespaces]);
23216
+ const resolved = await resolveResourceNamespaces(localConfig);
23217
+ if (!resolved) return null;
23218
+ const { activeNamespaces, allSkillNamespaces } = resolved;
22968
23219
  const inactiveSkillNamespaces = [...allSkillNamespaces].filter((namespace) => !activeNamespaces.skills.includes(namespace));
22969
23220
  const activeSkillNames = /* @__PURE__ */ new Set();
22970
23221
  const inactiveSkillNames = /* @__PURE__ */ new Set();
@@ -22997,6 +23248,20 @@ function filterRulesByKnowledgeNamespaces(rules, knowledgeNamespaces) {
22997
23248
  return knowledgeNamespaces.includes(namespace);
22998
23249
  });
22999
23250
  }
23251
+ function filterAgentsByNamespaces(agents, agentNamespaces) {
23252
+ const kept = agentNamespaces ? agents.filter((agent) => !agent.namespace || agentNamespaces.includes(agent.namespace)) : agents;
23253
+ const seen = /* @__PURE__ */ new Map();
23254
+ for (const agent of kept) {
23255
+ const existing = seen.get(agent.name);
23256
+ if (existing) {
23257
+ throw new Error(
23258
+ `Duplicate agent "${agent.name}" found in active namespaces "${existing.namespace ?? "(root)"}" and "${agent.namespace ?? "(root)"}"`
23259
+ );
23260
+ }
23261
+ seen.set(agent.name, agent);
23262
+ }
23263
+ return kept;
23264
+ }
23000
23265
  async function scanRoleAwareSkills(localConfig, namespaces) {
23001
23266
  const items = /* @__PURE__ */ new Map();
23002
23267
  for (const namespace of namespaces.skills) {
@@ -23098,6 +23363,42 @@ async function getInstalledResourceTargets(teamConfig, localConfig) {
23098
23363
  }
23099
23364
  return targets.sort();
23100
23365
  }
23366
+ function tombstoneExtensions(type, tool) {
23367
+ if (type === "rules") return [.../* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"])];
23368
+ if (type === "agents") return AGENT_FILE_EXTENSIONS;
23369
+ return [""];
23370
+ }
23371
+ async function cleanupTombstonedResources(freshConfig, localConfig, scopeLabel) {
23372
+ const tombstoneTypes = [
23373
+ { type: "rules", toolPathField: "rules" },
23374
+ { type: "skills", toolPathField: "skills" },
23375
+ { type: "agents", toolPathField: "agents" }
23376
+ ];
23377
+ const baseDir = resolveBaseDir(localConfig);
23378
+ for (const { type, toolPathField } of tombstoneTypes) {
23379
+ const handler = getHandler(type);
23380
+ const tombstones = await handler.readTombstones(localConfig);
23381
+ if (tombstones.size === 0) continue;
23382
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
23383
+ const dir = toolPath[toolPathField];
23384
+ if (!dir) continue;
23385
+ if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
23386
+ if (isAgentExcluded(localConfig, tool)) continue;
23387
+ for (const name of tombstones) {
23388
+ for (const extension of tombstoneExtensions(type, tool)) {
23389
+ const localPath = path66.join(baseDir, dir, `${name}${extension}`);
23390
+ if (!await pathExists(localPath)) continue;
23391
+ if (type === "skills" && await hasVcsMetadataRecursive(localPath)) {
23392
+ log.warn(`[${scopeLabel}] Kept tombstoned skill "${name}" (${tool}): it has local VCS metadata (.git) that may hold unpushed history. Back it up, then delete it manually.`);
23393
+ continue;
23394
+ }
23395
+ await remove(localPath);
23396
+ log.debug(`[${scopeLabel}] Cleaned up tombstoned ${type} ${name} from ${dir}`);
23397
+ }
23398
+ }
23399
+ }
23400
+ }
23401
+ }
23101
23402
  async function pullForScope(localConfig, options, policy = {}) {
23102
23403
  const scopeLabel = localConfig.scope;
23103
23404
  const revisionField = policy.revisionField ?? "lastPullRev";
@@ -23151,6 +23452,7 @@ async function pullForScope(localConfig, options, policy = {}) {
23151
23452
  } catch {
23152
23453
  }
23153
23454
  await injectRecallBlockIntoTools(freshConfig, localConfig, scopeLabel);
23455
+ await cleanupTombstonedResources(freshConfig, localConfig, scopeLabel);
23154
23456
  return;
23155
23457
  }
23156
23458
  log.debug(`[${scopeLabel}] Repo unchanged; resource target set changed, syncing`);
@@ -23223,6 +23525,11 @@ async function pullForScope(localConfig, options, policy = {}) {
23223
23525
  desiredSkillNames = new Set(items.map((i) => i.name));
23224
23526
  knownRepoSkillNames = new Set(allTeamSkills.map((i) => i.name));
23225
23527
  knownRepoSkillSources = new Map(allTeamSkills.map((i) => [i.name, i.sourcePath]));
23528
+ } else if (type === "agents") {
23529
+ items = filterAgentsByNamespaces(
23530
+ await handler.scanTeamForPull(freshConfig, localConfig),
23531
+ roleContext ? roleContext.activeNamespaces.agents : null
23532
+ );
23226
23533
  } else {
23227
23534
  items = await handler.scanTeamForPull(freshConfig, localConfig);
23228
23535
  }
@@ -23281,37 +23588,7 @@ async function pullForScope(localConfig, options, policy = {}) {
23281
23588
  totalSynced += items.length;
23282
23589
  }
23283
23590
  if (!options.dryRun) {
23284
- const tombstoneTypes = [
23285
- { type: "rules", ext: ".md", toolPathField: "rules" },
23286
- { type: "skills", toolPathField: "skills" },
23287
- { type: "agents", ext: ".md", toolPathField: "agents" }
23288
- ];
23289
- const baseDir = resolveBaseDir(localConfig);
23290
- for (const { type, ext, toolPathField } of tombstoneTypes) {
23291
- const handler = getHandler(type);
23292
- const tombstones = await handler.readTombstones(localConfig);
23293
- if (tombstones.size === 0) continue;
23294
- for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
23295
- const dir = toolPath[toolPathField];
23296
- if (!dir) continue;
23297
- if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
23298
- if (isAgentExcluded(localConfig, tool)) continue;
23299
- const extensions = type === "rules" ? [.../* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"])] : [ext];
23300
- for (const name of tombstones) {
23301
- for (const extension of extensions) {
23302
- const localPath = path66.join(baseDir, dir, extension ? `${name}${extension}` : name);
23303
- if (await pathExists(localPath)) {
23304
- if (type === "skills" && await hasVcsMetadataRecursive(localPath)) {
23305
- log.warn(`[${scopeLabel}] Kept tombstoned skill "${name}" (${tool}): it has local VCS metadata (.git) that may hold unpushed history. Back it up, then delete it manually.`);
23306
- continue;
23307
- }
23308
- await remove(localPath);
23309
- log.debug(`[${scopeLabel}] Cleaned up tombstoned ${type} ${name} from ${dir}`);
23310
- }
23311
- }
23312
- }
23313
- }
23314
- }
23591
+ await cleanupTombstonedResources(freshConfig, localConfig, scopeLabel);
23315
23592
  if (roleContext) {
23316
23593
  await cleanupInactiveNamespaceSkills(
23317
23594
  freshConfig,
@@ -23320,6 +23597,11 @@ async function pullForScope(localConfig, options, policy = {}) {
23320
23597
  roleContext.inactiveSkillNames,
23321
23598
  roleContext.inactiveSkillSources
23322
23599
  );
23600
+ await getHandler("agents").cleanupInactiveNamespaces(
23601
+ freshConfig,
23602
+ localConfig,
23603
+ roleContext.activeNamespaces.agents
23604
+ );
23323
23605
  }
23324
23606
  }
23325
23607
  if (!options.dryRun && desiredSkillNames && knownRepoSkillNames) {
@@ -24034,11 +24316,11 @@ var init_pull = __esm({
24034
24316
  init_resources();
24035
24317
  init_base();
24036
24318
  init_rule_format();
24319
+ init_agent_format();
24037
24320
  init_tags();
24038
24321
  init_builtin_skills();
24039
24322
  init_types();
24040
- init_roles();
24041
- init_projects();
24323
+ init_resource_namespaces();
24042
24324
  init_home();
24043
24325
  init_update();
24044
24326
  init_learnings_mirror();
@@ -24898,17 +25180,24 @@ async function doctor(options) {
24898
25180
  log.info("Running diagnostics...\n");
24899
25181
  const projectConfig = await detectProjectConfig();
24900
25182
  const localConfig = projectConfig ?? await loadLocalConfig();
24901
- const scope = localConfig?.scope ?? "user";
24902
- const configPathLabel = projectConfig ? `${projectConfig.projectRoot}/.teamai/config.yaml` : "~/.teamai/config.yaml";
24903
- console.log(` Scope: ${scope}${scope === "project" && localConfig?.projectRoot ? ` (${localConfig.projectRoot})` : ""}
24904
- `);
24905
- let teamConfig = null;
24906
- if (localConfig) {
24907
- teamConfig = await loadTeamConfig(localConfig.repo.localPath);
25183
+ if (!localConfig) {
25184
+ console.log(" Scope: not initialized\n");
25185
+ console.log(" \u2716 TeamAI is not initialized");
25186
+ console.log(" \u2192 Run `teamai init <repo-url>` in a project, or add `--scope user` for all projects");
25187
+ console.log("");
25188
+ log.warn("Initialization is required before diagnostics can run.");
25189
+ return false;
24908
25190
  }
24909
- const toolPaths = teamConfig?.toolPaths ?? TeamaiConfigSchema.shape.toolPaths.parse(void 0);
24910
- const providerName = teamConfig?.provider ?? "tgit";
24911
- const baseDir = localConfig ? resolveHookScope(localConfig).baseDir : getUserHome();
25191
+ const scope = localConfig.scope ?? "user";
25192
+ const scopeLabel = `${scope}${scope === "project" && localConfig.projectRoot ? ` (${localConfig.projectRoot})` : ""}`;
25193
+ console.log(` Scope: ${scopeLabel}
25194
+ `);
25195
+ const teamConfig = await loadTeamConfig(localConfig.repo.localPath);
25196
+ const toolPaths = teamConfig ? Object.fromEntries(
25197
+ Object.entries(scopedToolPaths(teamConfig, localConfig)).filter(([tool]) => !isAgentExcluded(localConfig, tool))
25198
+ ) : {};
25199
+ const providerName = teamConfig?.provider;
25200
+ const baseDir = resolveHookScope(localConfig).baseDir;
24912
25201
  const checks = [];
24913
25202
  if (providerName === "tgit") {
24914
25203
  const { isGfInstalled: isGfInstalled2, gfIsAuthenticated: gfIsAuthenticated2 } = await Promise.resolve().then(() => (init_tgit(), tgit_exports));
@@ -24954,23 +25243,14 @@ async function doctor(options) {
24954
25243
  });
24955
25244
  }
24956
25245
  checks.push(
24957
- {
24958
- name: `Local config exists (${configPathLabel})`,
24959
- check: async () => localConfig !== null,
24960
- fix: "Run `teamai init` to initialize"
24961
- },
24962
25246
  {
24963
25247
  name: "Team repo exists locally",
24964
- check: async () => {
24965
- if (!localConfig) return false;
24966
- return pathExists(localConfig.repo.localPath);
24967
- },
25248
+ check: async () => pathExists(localConfig.repo.localPath),
24968
25249
  fix: "Run `teamai init` to clone the team repo"
24969
25250
  },
24970
25251
  {
24971
25252
  name: "Team config (teamai.yaml) is valid",
24972
25253
  check: async () => {
24973
- if (!localConfig) return false;
24974
25254
  const config = await loadTeamConfig(localConfig.repo.localPath);
24975
25255
  return config !== null;
24976
25256
  },
@@ -24981,7 +25261,6 @@ async function doctor(options) {
24981
25261
  name: "Env variables injected in shell profile",
24982
25262
  check: async () => {
24983
25263
  if (teamConfig?.sharing?.env?.injectShellProfile === false) return true;
24984
- if (!localConfig) return true;
24985
25264
  const envYamlPath = path70.join(localConfig.repo.localPath, "env", "env.yaml");
24986
25265
  if (!await pathExists(envYamlPath)) return true;
24987
25266
  const home = getUserHome();
@@ -25010,13 +25289,11 @@ async function doctor(options) {
25010
25289
  allPassed = false;
25011
25290
  }
25012
25291
  }
25013
- if (localConfig) {
25014
- const { pkgDoctorReport: pkgDoctorReport2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
25015
- const packageReport = await pkgDoctorReport2(localConfig, process.cwd());
25016
- if (packageReport) {
25017
- for (const line of packageReport.lines) console.log(line);
25018
- if (!packageReport.allPassed) allPassed = false;
25019
- }
25292
+ const { pkgDoctorReport: pkgDoctorReport2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
25293
+ const packageReport = await pkgDoctorReport2(localConfig, process.cwd());
25294
+ if (packageReport) {
25295
+ for (const line of packageReport.lines) console.log(line);
25296
+ if (!packageReport.allPassed) allPassed = false;
25020
25297
  }
25021
25298
  if (await hasInstalledCodexHooks(toolPaths, baseDir)) {
25022
25299
  console.log("");
@@ -25028,6 +25305,7 @@ async function doctor(options) {
25028
25305
  } else {
25029
25306
  log.warn("Some checks failed. See suggestions above.");
25030
25307
  }
25308
+ return allPassed;
25031
25309
  }
25032
25310
  var init_doctor = __esm({
25033
25311
  "src/doctor.ts"() {
@@ -25152,7 +25430,8 @@ async function rolesInit(options) {
25152
25430
  description,
25153
25431
  resources: {
25154
25432
  knowledge: namespaces,
25155
- skills: namespaces
25433
+ skills: namespaces,
25434
+ agents: namespaces
25156
25435
  }
25157
25436
  });
25158
25437
  log.success(`Added role: ${id} (namespaces: ${namespaces.join(", ")})`);
@@ -25296,7 +25575,8 @@ async function rolesAdd(roleId, options) {
25296
25575
  description: options.description ?? "",
25297
25576
  resources: {
25298
25577
  knowledge: namespaces,
25299
- skills: namespaces
25578
+ skills: namespaces,
25579
+ agents: namespaces
25300
25580
  }
25301
25581
  };
25302
25582
  const updatedManifest = {
@@ -25410,7 +25690,8 @@ async function rolesUpdate(roleId, options) {
25410
25690
  description: hasDesc ? options.description : existingRole.description,
25411
25691
  resources: {
25412
25692
  knowledge: updatedNamespaces,
25413
- skills: updatedNamespaces
25693
+ skills: updatedNamespaces,
25694
+ agents: updatedNamespaces
25414
25695
  }
25415
25696
  };
25416
25697
  const updatedManifest = {
@@ -25833,10 +26114,13 @@ async function collectTeamRuleNames(repoPath) {
25833
26114
  async function collectTeamAgentNames(repoPath) {
25834
26115
  const teamAgentsDir = path74.join(repoPath, "agents");
25835
26116
  if (!await pathExists(teamAgentsDir)) return /* @__PURE__ */ new Set();
25836
- const files = await listFiles(teamAgentsDir);
25837
- return new Set(
25838
- files.filter((file) => file.endsWith(".yaml") || file.endsWith(".md")).map((file) => path74.basename(file).replace(/\.(yaml|md)$/, ""))
25839
- );
26117
+ const names = /* @__PURE__ */ new Set();
26118
+ for (const { dir } of await listTeamAgentDirs(teamAgentsDir)) {
26119
+ for (const file of await listFiles(dir)) {
26120
+ if (file.endsWith(".yaml") || file.endsWith(".md")) names.add(file.replace(/\.(yaml|md)$/, ""));
26121
+ }
26122
+ }
26123
+ return names;
25840
26124
  }
25841
26125
  function isEmptyHooksResidue(parsed) {
25842
26126
  if (parsed == null || !("hooks" in parsed) || typeof parsed.hooks !== "object" || parsed.hooks == null) return false;
@@ -25936,8 +26220,8 @@ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, te
25936
26220
  const agentsDir = path74.join(baseDir, toolPath.agents);
25937
26221
  if (await pathExists(agentsDir)) {
25938
26222
  for (const file of await listFiles(agentsDir)) {
25939
- if (!file.endsWith(".md") && !file.endsWith(".toml") && !file.endsWith(".json")) continue;
25940
- const name = path74.basename(file).replace(/\.(md|toml|json)$/, "");
26223
+ const name = agentStemFromFilename(path74.basename(file));
26224
+ if (name === null) continue;
25941
26225
  if (!teamAgentNames.has(name) && !BUILTIN_AGENT_NAMES.has(name)) continue;
25942
26226
  res.agentFiles.push(path74.join(agentsDir, file));
25943
26227
  }
@@ -26415,6 +26699,8 @@ var init_uninstall = __esm({
26415
26699
  init_types();
26416
26700
  init_builtin_rules();
26417
26701
  init_rule_format();
26702
+ init_agent_format();
26703
+ init_agents();
26418
26704
  init_builtin_agents();
26419
26705
  init_builtin_skills();
26420
26706
  init_fs();
@@ -26967,20 +27253,25 @@ async function saveSession(options) {
26967
27253
  if (usesReportsBranch(localConfig)) {
26968
27254
  const spin2 = spinner("Pushing session summary to team...").start();
26969
27255
  try {
26970
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
26971
- const wt = await ensureReportsWorktree2(localConfig);
26972
- const teamDir2 = path79.join(wt, "sessions", username);
26973
- const written = await appendMonthlyLog(teamDir2, summary, { includePrompt: options.includePrompt });
26974
- if (!written) {
26975
- spin2.info("Session already present in the team log \u2014 nothing to push.");
26976
- return;
26977
- }
26978
- const rel = path79.relative(wt, written);
27256
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
27257
+ let rel;
27258
+ let ran = false;
26979
27259
  const pushed = await withTimeout(
26980
- commitAndPushReports2(localConfig, commitMsg, [rel]),
27260
+ updateReports2(localConfig, async (wt) => {
27261
+ ran = true;
27262
+ const teamDir2 = path79.join(wt, "sessions", username);
27263
+ const written = await appendMonthlyLog(teamDir2, summary, { includePrompt: options.includePrompt });
27264
+ if (!written) return null;
27265
+ rel = path79.relative(wt, written);
27266
+ return { files: [rel], message: commitMsg };
27267
+ }),
26981
27268
  1e4,
26982
27269
  "Push timeout (10s)"
26983
27270
  );
27271
+ if (ran && !rel) {
27272
+ spin2.info("Session already present in the team log \u2014 nothing to push.");
27273
+ return;
27274
+ }
26984
27275
  if (pushed) spin2.succeed(`Pushed: ${rel}`);
26985
27276
  else spin2.info("Nothing new to push.");
26986
27277
  } catch (e) {
@@ -31104,11 +31395,13 @@ var init_hook_handlers = __esm({
31104
31395
  const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31105
31396
  if (usesReportsBranch2(localConfig)) {
31106
31397
  try {
31107
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
31108
- const wt = await ensureReportsWorktree2(localConfig);
31109
- const synced = await syncVotesToTeam2(wt, localConfig.username, votesDir);
31110
- if (synced) {
31111
- await commitAndPushReports2(localConfig, `[teamai] Update votes for ${localConfig.username}`, [`votes/${localConfig.username}.yaml`]);
31398
+ const { hasPendingVoteDeltas: hasPendingVoteDeltas2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
31399
+ if (await hasPendingVoteDeltas2(votesDir, localConfig.username)) {
31400
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
31401
+ await updateReports2(localConfig, async (wt) => await syncVotesToTeam2(wt, localConfig.username, votesDir) ? {
31402
+ files: [`votes/${localConfig.username}.yaml`],
31403
+ message: `[teamai] Update votes for ${localConfig.username}`
31404
+ } : null);
31112
31405
  }
31113
31406
  } catch {
31114
31407
  }
@@ -34828,7 +35121,7 @@ var init_codebase = __esm({
34828
35121
 
34829
35122
  // src/wiki-engine/code-knowledge/code-collector.ts
34830
35123
  import { createHash as createHash5 } from "crypto";
34831
- import { execFile as execFile4 } from "child_process";
35124
+ import { execFile as execFile3 } from "child_process";
34832
35125
  import { readFile as readFile6, readdir as readdir2, stat as stat2 } from "fs/promises";
34833
35126
  import path105 from "path";
34834
35127
  import { promisify as promisify3 } from "util";
@@ -34984,7 +35277,7 @@ var init_code_collector = __esm({
34984
35277
  "src/wiki-engine/code-knowledge/code-collector.ts"() {
34985
35278
  "use strict";
34986
35279
  init_wiki_protocol();
34987
- execFileAsync3 = promisify3(execFile4);
35280
+ execFileAsync3 = promisify3(execFile3);
34988
35281
  KEY_FILE_PATTERNS = {
34989
35282
  go: [/main\.go$/, /cmd\/.*\.go$/, /handler.*\.go$/, /server\.go$/, /router\.go$/],
34990
35283
  python: [/main\.py$/, /app\.py$/, /server\.py$/, /routes?\.py$/, /models?\.py$/],
@@ -42953,7 +43246,7 @@ program.name("teamai").description("TeamAI \u2014 Make Every Team AI Native").ve
42953
43246
  process.exit(1);
42954
43247
  }
42955
43248
  });
42956
- program.command("init").description("Initialize teamai (configure Git provider, clone repo, register member)").argument("[repo]", 'Team repo (owner/repo or full URL). Pass "." for single-repo mode (the current git repo is the team repo).').option("--repo <repo>", "Team repo (alias of the positional argument)").option("--http <url>", "Git-free HTTP team repo (read-only consumer; only needs an API key)").option("--self", "Single-repo mode: the current git repo is the team repo (equivalent to `teamai init .`). Knowledge lives on main under .teamai/; reports go to the teamai-reports orphan branch.").option("--token <key>", "API key for HTTP team repo / status reporting (stored 0600, never committed). Also reads TEAMAI_API_TOKEN.").option("--scope <scope>", "Install scope: project (default, <cwd>/.teamai + <cwd>/.claude) or user (~/.teamai + ~/.claude)").option("--inherit-user-scope", "In project scope, also sync safe user-scope resources and search its knowledge").option("--no-inherit-user-scope", "Disable user-scope inheritance for this project").option("--role <id>", "Primary role ID (e.g. hai_dev) for non-interactive setup").option("--project <ids>", "Active logical project(s) from manifest/projects.yaml (comma-separated); scopes which project resources and learnings this directory syncs").option("--agent <name>", "AI tools to set up (e.g. claude, codex, cursor, codebuddy, workbuddy, dsh). Repeatable or comma-separated. In single-repo mode, selects which tool dirs to create; omit for an interactive picker. Additive on repeated runs.", (val, acc) => acc.concat(val), []).option("--force", "Overwrite existing config without confirmation").action(async (repoArg, cmdOpts) => {
43249
+ program.command("init").description("Initialize teamai (configure Git provider, clone repo, register member)").argument("[repo]", 'Team repo (owner/repo or full URL). Pass "." for single-repo mode (the current git repo is the team repo).').option("--repo <repo>", "Team repo (alias of the positional argument)").option("--http <url>", "Git-free HTTP team repo (read-only consumer; only needs an API key)").option("--self", "Single-repo mode: the current git repo is the team repo (equivalent to `teamai init .`). Knowledge lives on main under .teamai/; reports go to the teamai-reports orphan branch.").option("--token <key>", "API key for HTTP team repo / status reporting (stored 0600, never committed). Also reads TEAMAI_API_TOKEN.").option("--scope <scope>", "Install scope: project (default, <cwd>/.teamai + <cwd>/.claude) or user (~/.teamai + ~/.claude)").option("--inherit-user-scope", "In project scope, also sync safe user-scope resources and search its knowledge").option("--no-inherit-user-scope", "Disable user-scope inheritance for this project").option("--role <id>", "Primary role ID (e.g. hai_dev) for non-interactive setup").option("--project <ids>", 'Active logical project(s) from manifest/projects.yaml (comma-separated); scopes which project resources and learnings this directory syncs. Pass "all" to activate every project the manifest declares (a snapshot taken now)').option("--agent <name>", "AI tools to set up (e.g. claude, codex, cursor, codebuddy, workbuddy, dsh). Repeatable or comma-separated. In single-repo mode, selects which tool dirs to create; omit for an interactive picker. Additive on repeated runs.", (val, acc) => acc.concat(val), []).option("--force", "Overwrite existing config without confirmation").action(async (repoArg, cmdOpts) => {
42957
43250
  const globalOpts = program.opts();
42958
43251
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42959
43252
  await init2({ ...globalOpts, ...cmdOpts, repoPositional: repoArg });
@@ -43033,7 +43326,8 @@ registerPackagesCommand(program);
43033
43326
  program.command("doctor").description("Diagnose configuration issues").action(async () => {
43034
43327
  const globalOpts = program.opts();
43035
43328
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
43036
- await doctor2(globalOpts);
43329
+ const allPassed = await doctor2(globalOpts);
43330
+ if (!allPassed) process.exitCode = 1;
43037
43331
  });
43038
43332
  var rolesCmd = program.command("roles").description("Manage team roles and resource namespaces").action(async () => {
43039
43333
  const globalOpts = program.opts();
@@ -43334,7 +43628,7 @@ program.command("todowrite-hint", { hidden: true }).description("Remind the agen
43334
43628
  await todoWriteHint2();
43335
43629
  }
43336
43630
  });
43337
- program.command("import").description("Import knowledge from local directories, remote repos, organizations, MRs, or iWiki").option("--dir <path>", "Extract code knowledge from a local directory (same as --from-repo but no clone)").addOption(new Option("--from-claude", "Scan Claude/Cursor rule directories (~/.claude/rules, ~/.cursor/rules)").hideHelp()).option("--from-mr <url>", "Extract learning from merged MR/PR and trigger incremental teamwiki update").option("--from-iwiki <space-id-or-url>", "Import documents from iWiki Space ID or page URL (requires TAI_PAT_TOKEN)").addOption(new Option("--resume", "Resume an interrupted import session").hideHelp()).option("--all", "Accept all suggestions without interactive confirmation").addOption(new Option("--output <path>", "Write drafts to this directory instead of pushing to team repo").hideHelp()).option("--from-repo <url>", "Clone a remote repo and generate per-repo codebase summary").addOption(new Option("--ssh", "Force SSH clone even if HTTPS token is available").hideHelp()).addOption(new Option("--domain <name>", "Skip AI recommendation and assign repo to this domain explicitly").hideHelp()).option("--from-repo-list <path>", "Batch import repos from a YAML whitelist").addOption(new Option("--concurrency <n>", "Concurrent repos for --from-repo-list (default 3)").default("3").hideHelp()).option("--incremental", "Use cached clone with fetch+reset (with --from-repo or --from-repo-list)").option("--skip-enrich", "Skip AI enrichment (only clone + extract + graph, no LLM calls)").option("--from-org <org>", "List repos under an org and generate a repo whitelist").addOption(new Option("--max-repos <n>", "Cap on repos pulled from --from-org (default 200)").default("200").hideHelp()).addOption(new Option("--exclude-archived", "Exclude archived repos from --from-org (default true)").hideHelp()).addOption(new Option("--include-pattern <re>", "Regex to include repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--exclude-pattern <re>", "Regex to exclude repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--skip-import", "Only write drafts; skip the actual --from-repo-list run").hideHelp()).addOption(new Option("--iwiki-dual", "Enable dual-output mode for --from-iwiki (write codebase sections in addition to learning)").hideHelp()).addOption(new Option("--require-review", "Defer codebase section writes to .teamai/pending-review.jsonl for human review").hideHelp()).option("--cache-status", "Show import cache status (repos cached, disk usage)").option("--cache-gc", "Garbage-collect stale import cache entries").addOption(new Option("--max-bytes <n>", "Override capacity cap for --cache-gc").hideHelp()).addOption(new Option("--stale-days <n>", "Threshold for stale-eviction in days (default 30)").default("30").hideHelp()).action(async (cmdOpts) => {
43631
+ program.command("import").description("Import knowledge from local directories, remote repos, organizations, MRs, or iWiki").option("--dir <path>", "Extract code knowledge from a local directory (same as --from-repo but no clone)").addOption(new Option("--from-claude", "Scan Claude/Cursor rule directories (~/.claude/rules, ~/.cursor/rules)").hideHelp()).option("--from-mr <url>", "Extract learning from merged MR/PR and trigger incremental teamwiki update").option("--from-iwiki <space-id-or-url>", "Import documents from iWiki Space ID or page URL (requires TAI_PAT_TOKEN)").addOption(new Option("--resume", "Resume an interrupted import session").hideHelp()).option("--all", "Accept all suggestions without interactive confirmation").addOption(new Option("--output <path>", "Write drafts to this directory instead of pushing to team repo").hideHelp()).option("--from-repo <url>", "Clone a remote repo and generate per-repo codebase summary").addOption(new Option("--ssh", "Force SSH clone even if HTTPS token is available").hideHelp()).addOption(new Option("--domain <name>", "Skip AI recommendation and assign repo to this domain explicitly").hideHelp()).option("--from-repo-list <path>", "Batch import repos from a YAML whitelist").addOption(new Option("--concurrency <n>", "Concurrent repos for --from-repo-list (default 3)").default("3").hideHelp()).option("--incremental", "Use cached clone with fetch+reset (with --from-repo or --from-repo-list)").option("--skip-enrich", "Skip AI enrichment (only clone + extract + graph, no LLM calls)").option("--from-org <org>", "List repos under an org and generate a repo whitelist").addOption(new Option("--max-repos <n>", "Cap on repos pulled from --from-org (default 200)").default("200").hideHelp()).addOption(new Option("--exclude-archived", "Exclude archived repos from --from-org (default true)").hideHelp()).addOption(new Option("--include-pattern <re>", "Regex to include repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--exclude-pattern <re>", "Regex to exclude repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--skip-import", "Only write drafts; skip the actual --from-repo-list run").hideHelp()).addOption(new Option("--iwiki-dual", "Enable dual-output mode for --from-iwiki (write codebase sections in addition to learning)").hideHelp()).addOption(new Option("--require-review", "Defer codebase section writes to .teamai/pending-review.jsonl for human review").hideHelp()).option("--cache-status", "Show import cache status (repos cached, disk usage)").option("--cache-gc", "Garbage-collect stale import cache entries").option("--json", "Output cache status or GC result as JSON").addOption(new Option("--max-bytes <n>", "Override capacity cap for --cache-gc").hideHelp()).addOption(new Option("--stale-days <n>", "Threshold for stale-eviction in days (default 30)").default("30").hideHelp()).action(async (cmdOpts) => {
43338
43632
  const globalOpts = program.opts();
43339
43633
  if (cmdOpts.cacheStatus || cmdOpts.cacheGc) {
43340
43634
  const { cacheCmd: cacheCmd2 } = await Promise.resolve().then(() => (init_cache_cmd(), cache_cmd_exports));