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

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
@@ -503,6 +503,7 @@ __export(types_exports, {
503
503
  CONTRIBUTE_SKILL_BONUS: () => CONTRIBUTE_SKILL_BONUS,
504
504
  CONTRIBUTE_SMART_THRESHOLD: () => CONTRIBUTE_SMART_THRESHOLD,
505
505
  CONTRIBUTE_TOOLERROR_TIERS: () => CONTRIBUTE_TOOLERROR_TIERS,
506
+ COPILOT_TOOL_ID: () => COPILOT_TOOL_ID,
506
507
  CORRECTION_KEYWORDS: () => CORRECTION_KEYWORDS,
507
508
  CORRECTION_WINDOW_MS: () => CORRECTION_WINDOW_MS,
508
509
  CultureCompanySchema: () => CultureCompanySchema,
@@ -556,6 +557,7 @@ __export(types_exports, {
556
557
  areTeamHooksDisabled: () => areTeamHooksDisabled,
557
558
  emptyTokenUsage: () => emptyTokenUsage,
558
559
  getConfigPath: () => getConfigPath,
560
+ getCopilotHome: () => getCopilotHome,
559
561
  getDataHome: () => getDataHome,
560
562
  getEnvBackupPath: () => getEnvBackupPath,
561
563
  getHooksSharing: () => getHooksSharing,
@@ -590,6 +592,7 @@ __export(types_exports, {
590
592
  resolveCoAuthor: () => resolveCoAuthor,
591
593
  resolveHookScope: () => resolveHookScope,
592
594
  resolveLegacyProjectHookScope: () => resolveLegacyProjectHookScope,
595
+ resolveToolBaseDir: () => resolveToolBaseDir,
593
596
  scopedToolPaths: () => scopedToolPaths,
594
597
  totalTokens: () => totalTokens,
595
598
  usesReportsBranch: () => usesReportsBranch
@@ -699,6 +702,14 @@ function resolveBaseDir(localConfig) {
699
702
  }
700
703
  return getUserHome();
701
704
  }
705
+ function getCopilotHome(env = process.env) {
706
+ const configured = env.COPILOT_HOME?.trim();
707
+ return configured ? path3.resolve(configured) : path3.join(getUserHome(), DEFAULT_COPILOT_HOME);
708
+ }
709
+ function resolveToolBaseDir(tool, localConfig) {
710
+ if (tool === COPILOT_TOOL_ID && localConfig.scope === "user") return getCopilotHome();
711
+ return resolveBaseDir(localConfig);
712
+ }
702
713
  function isAgentDisabled(localConfig, tool) {
703
714
  return localConfig.disabledAgents?.includes(tool) ?? false;
704
715
  }
@@ -719,7 +730,8 @@ function scopedToolPaths(teamConfig, localConfig) {
719
730
  ...paths,
720
731
  ...us.skills !== void 0 ? { skills: us.skills } : {},
721
732
  ...us.rules !== void 0 ? { rules: us.rules } : {},
722
- ...us.agents !== void 0 ? { agents: us.agents } : {}
733
+ ...us.agents !== void 0 ? { agents: us.agents } : {},
734
+ ...us.hooks !== void 0 ? { hooks: us.hooks } : {}
723
735
  };
724
736
  }
725
737
  return out;
@@ -795,7 +807,7 @@ function getPushignorePath() {
795
807
  function areTeamHooksDisabled() {
796
808
  return process.env.TEAMAI_HOOKS_DISABLED === "1" || process.env.TEAMAI_HOOKS_DISABLED === "true";
797
809
  }
798
- var ToolPathsSchema, ScopeEnum, SharingConfigSchema, SourceConfigSchema, SOURCE_PULL_TTL_MS, TEAMAI_SOURCES_DIR, TeamaiConfigSchema, MemberConfigSchema, LocalConfigSchema, PendingPushItemSchema, PendingPushSchema, StateSchema, RESOURCE_TYPES, TEAMAI_RULES_START, TEAMAI_RULES_END, TEAMAI_HOOK_DESCRIPTION_PREFIX, TEAMAI_CUSTOM_HOOK_PREFIX, TEAMAI_AGENT_HOOK_PREFIX, TEAMAI_ENV_START, TEAMAI_ENV_END, TEAMAI_CULTURE_START, TEAMAI_CULTURE_END, TEAMAI_CLAUDEMD_START, TEAMAI_CLAUDEMD_END, TEAMAI_RECALL_RULES_START, TEAMAI_RECALL_RULES_END, SKILL_NAME_REGEX, UsageEventSchema, DASHBOARD_DEFAULT_PORT, DASHBOARD_IDLE_TIMEOUT_MS, DASHBOARD_STALE_TIMEOUT_MS, DASHBOARD_COMPACTION_THRESHOLD, DASHBOARD_STOPPED_DISPLAY_MS, DASHBOARD_PID_CHECK_INTERVAL_MS, CORRECTION_WINDOW_MS, CORRECTION_KEYWORDS, INTERVENTION_SCAN_MAX_BYTES, TRANSCRIPT_INTERRUPT_PREFIX, TRANSCRIPT_SYSTEM_PREFIXES, TRANSCRIPT_REJECT_MARKERS, CONTRIBUTE_BASE_THRESHOLD, CONTRIBUTE_SMART_THRESHOLD, CONTRIBUTE_INTERRUPT_WEIGHT, CONTRIBUTE_REJECT_WEIGHT, CONTRIBUTE_CORRECTION_WEIGHT, CONTRIBUTE_TOOLERROR_TIERS, CONTRIBUTE_SKILL_BONUS, CONTRIBUTE_DIVERSITY_BONUS_MAX, CONTRIBUTE_FASTPATH_TTL_MS, CONTRIBUTE_KNOWLEDGE_GAP_BONUS, CONTRIBUTE_LOW_QUALITY_BONUS, CONTRIBUTE_LOW_QUALITY_THRESHOLD, CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT, SEARCH_INDEX_VERSION, CultureCompanySchema, CultureTeamSchema, CultureFrontmatterSchema, REPORTS_BRANCH, REPORTS_WORKTREE_DIRNAME, KNOWLEDGE_WORKTREE_DIRNAME, REPORTS_LOCK_FILENAME, BOOTSTRAP_LOCK_FILENAME, SYNC_LOCK_FILENAME;
810
+ var ToolPathsSchema, ScopeEnum, SharingConfigSchema, SourceConfigSchema, SOURCE_PULL_TTL_MS, TEAMAI_SOURCES_DIR, TeamaiConfigSchema, MemberConfigSchema, LocalConfigSchema, PendingPushItemSchema, PendingPushSchema, StateSchema, RESOURCE_TYPES, TEAMAI_RULES_START, TEAMAI_RULES_END, TEAMAI_HOOK_DESCRIPTION_PREFIX, TEAMAI_CUSTOM_HOOK_PREFIX, TEAMAI_AGENT_HOOK_PREFIX, TEAMAI_ENV_START, TEAMAI_ENV_END, TEAMAI_CULTURE_START, TEAMAI_CULTURE_END, TEAMAI_CLAUDEMD_START, TEAMAI_CLAUDEMD_END, TEAMAI_RECALL_RULES_START, TEAMAI_RECALL_RULES_END, SKILL_NAME_REGEX, UsageEventSchema, DASHBOARD_DEFAULT_PORT, DASHBOARD_IDLE_TIMEOUT_MS, DASHBOARD_STALE_TIMEOUT_MS, DASHBOARD_COMPACTION_THRESHOLD, DASHBOARD_STOPPED_DISPLAY_MS, DASHBOARD_PID_CHECK_INTERVAL_MS, CORRECTION_WINDOW_MS, CORRECTION_KEYWORDS, INTERVENTION_SCAN_MAX_BYTES, TRANSCRIPT_INTERRUPT_PREFIX, TRANSCRIPT_SYSTEM_PREFIXES, TRANSCRIPT_REJECT_MARKERS, CONTRIBUTE_BASE_THRESHOLD, CONTRIBUTE_SMART_THRESHOLD, CONTRIBUTE_INTERRUPT_WEIGHT, CONTRIBUTE_REJECT_WEIGHT, CONTRIBUTE_CORRECTION_WEIGHT, CONTRIBUTE_TOOLERROR_TIERS, CONTRIBUTE_SKILL_BONUS, CONTRIBUTE_DIVERSITY_BONUS_MAX, CONTRIBUTE_FASTPATH_TTL_MS, CONTRIBUTE_KNOWLEDGE_GAP_BONUS, CONTRIBUTE_LOW_QUALITY_BONUS, CONTRIBUTE_LOW_QUALITY_THRESHOLD, CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT, SEARCH_INDEX_VERSION, CultureCompanySchema, CultureTeamSchema, CultureFrontmatterSchema, COPILOT_TOOL_ID, DEFAULT_COPILOT_HOME, REPORTS_BRANCH, REPORTS_WORKTREE_DIRNAME, KNOWLEDGE_WORKTREE_DIRNAME, REPORTS_LOCK_FILENAME, BOOTSTRAP_LOCK_FILENAME, SYNC_LOCK_FILENAME;
799
811
  var init_types = __esm({
800
812
  "src/types.ts"() {
801
813
  "use strict";
@@ -804,6 +816,8 @@ var init_types = __esm({
804
816
  skills: z.string().optional(),
805
817
  rules: z.string().optional(),
806
818
  settings: z.string().optional(),
819
+ /** Standalone hooks file for tools that do not store hooks in settings. */
820
+ hooks: z.string().optional(),
807
821
  claudemd: z.string().optional(),
808
822
  /** Per-tool agents directory (Phase 1: teamai-recall subagent target).
809
823
  * Optional — tools without subagent support omit this and agents sync skips them. */
@@ -826,7 +840,8 @@ var init_types = __esm({
826
840
  userScope: z.object({
827
841
  skills: z.string().optional(),
828
842
  rules: z.string().optional(),
829
- agents: z.string().optional()
843
+ agents: z.string().optional(),
844
+ hooks: z.string().optional()
830
845
  }).optional()
831
846
  });
832
847
  ScopeEnum = z.enum(["user", "project"]);
@@ -954,6 +969,15 @@ var init_types = __esm({
954
969
  tclaude: { skills: ".tclaude/skills", rules: ".tclaude/rules", settings: ".tclaude/settings.json", claudemd: ".tclaude/CLAUDE.md", agents: ".tclaude/agents", mcp: ".tclaude/.claude.json" },
955
970
  tcodex: { skills: ".tcodex/skills", rules: ".tcodex/rules", settings: ".tcodex/hooks.json", agents: ".tcodex/agents" },
956
971
  cursor: { skills: ".cursor/skills", rules: ".cursor/rules", settings: ".cursor/hooks.json", agents: ".cursor/agents", mcp: ".cursor/mcp.json", mcpProject: ".cursor/mcp.json" },
972
+ // GitHub Copilot CLI keeps project customizations under .github and moves
973
+ // the complete user customization root when COPILOT_HOME is set. Hooks are
974
+ // a standalone file; settings.json is deliberately never managed.
975
+ copilot: {
976
+ skills: ".github/skills",
977
+ rules: ".github/instructions",
978
+ hooks: ".github/hooks/teamai.json",
979
+ userScope: { skills: "skills", rules: "instructions", hooks: "hooks/teamai.json" }
980
+ },
957
981
  // JoyCode: skills, rules (.mdc), and subagents are synced to .joycode/.
958
982
  // JoyCode currently does not provide a lifecycle hooks system or startup
959
983
  // adapter, so it intentionally has no `settings` path. Hook reconciliation
@@ -1238,6 +1262,8 @@ var init_types = __esm({
1238
1262
  company: CultureCompanySchema.optional(),
1239
1263
  team: CultureTeamSchema.optional()
1240
1264
  });
1265
+ COPILOT_TOOL_ID = "copilot";
1266
+ DEFAULT_COPILOT_HOME = ".copilot";
1241
1267
  REPORTS_BRANCH = "teamai-reports";
1242
1268
  REPORTS_WORKTREE_DIRNAME = "reports-wt";
1243
1269
  KNOWLEDGE_WORKTREE_DIRNAME = "knowledge-wt";
@@ -1344,7 +1370,8 @@ function resolveRoleResourceNamespaces(input) {
1344
1370
  skills: [],
1345
1371
  // Roles never contribute learnings namespaces; only projects do. Kept empty
1346
1372
  // so the shape matches project resolution for a clean union at the call site.
1347
- learnings: []
1373
+ learnings: [],
1374
+ agents: []
1348
1375
  };
1349
1376
  for (const type of ROLE_RESOURCE_TYPES) {
1350
1377
  const seen = /* @__PURE__ */ new Set();
@@ -1388,10 +1415,13 @@ var init_roles = __esm({
1388
1415
  "use strict";
1389
1416
  init_fs();
1390
1417
  init_logger();
1391
- ROLE_RESOURCE_TYPES = ["knowledge", "skills"];
1418
+ ROLE_RESOURCE_TYPES = ["knowledge", "skills", "agents"];
1392
1419
  RoleResourceNamespacesSchema = z2.object({
1393
1420
  knowledge: z2.array(z2.string().min(1)),
1394
1421
  skills: z2.array(z2.string().min(1)),
1422
+ // Optional: a role without `agents` receives root-level agents only, which
1423
+ // is what every manifest written before this key existed already got.
1424
+ agents: z2.array(z2.string().min(1)).default([]),
1395
1425
  // learnings is accepted for backward compatibility but ignored at runtime.
1396
1426
  // All learnings are shared flat across the entire team (no namespace isolation).
1397
1427
  learnings: z2.array(z2.string()).optional()
@@ -1583,7 +1613,7 @@ function getWrapperDispatchCommand(event, tool, matcher) {
1583
1613
  return `PATH="$HOME/${TEAMAI_BIN_DIR}:$PATH" teamai hook-dispatch ${event} --tool ${tool}${matcherArg} 2>/dev/null || true`;
1584
1614
  }
1585
1615
  function builtinHookDefs(tool) {
1586
- const withTimeout3 = tool === "cursor" || tool === "workbuddy" || tool === "codebuddy";
1616
+ const withTimeout3 = tool === "cursor" || tool === "copilot" || tool === "workbuddy" || tool === "codebuddy";
1587
1617
  const buildCommand = tool === "zcode" ? getRawDispatchCommand : WRAPPER_TOOLS.has(tool) ? getWrapperDispatchCommand : getDispatchCommand;
1588
1618
  return BUILTIN_HOOK_SPECS.map((spec) => ({
1589
1619
  source: "builtin",
@@ -1635,10 +1665,18 @@ function toolInstallRoot(toolPath) {
1635
1665
  }
1636
1666
  return segments[0] ?? toolPath;
1637
1667
  }
1668
+ async function isToolInstalledForConfig(tool, toolPath, localConfig) {
1669
+ const baseDir = resolveToolBaseDir(tool, localConfig);
1670
+ if (tool === COPILOT_TOOL_ID) {
1671
+ return localConfig.enabledAgents?.includes(COPILOT_TOOL_ID) === true || pathExists(getCopilotHome());
1672
+ }
1673
+ return ResourceHandler.isToolInstalled(toolPath, baseDir);
1674
+ }
1638
1675
  var TOMBSTONE_FILE, ResourceHandler;
1639
1676
  var init_base = __esm({
1640
1677
  "src/resources/base.ts"() {
1641
1678
  "use strict";
1679
+ init_types();
1642
1680
  init_fs();
1643
1681
  init_home();
1644
1682
  TOMBSTONE_FILE = ".removed";
@@ -2505,6 +2543,7 @@ var init_hermes_hooks = __esm({
2505
2543
  var hooks_exports = {};
2506
2544
  __export(hooks_exports, {
2507
2545
  AGENT_HOOK_EVENTS: () => AGENT_HOOK_EVENTS,
2546
+ CLAUDE_TO_COPILOT_EVENTS: () => CLAUDE_TO_COPILOT_EVENTS,
2508
2547
  CLAUDE_TO_CURSOR_EVENTS: () => CLAUDE_TO_CURSOR_EVENTS,
2509
2548
  OPENCLAW_TOOLS: () => OPENCLAW_TOOLS,
2510
2549
  TEAMAI_HOOK_SUBCOMMANDS: () => TEAMAI_HOOK_SUBCOMMANDS,
@@ -2530,6 +2569,7 @@ __export(hooks_exports, {
2530
2569
  import path14 from "path";
2531
2570
  import { realpathSync } from "fs";
2532
2571
  function detectFormat(tool) {
2572
+ if (tool === COPILOT_TOOL_ID) return "copilot";
2533
2573
  if (CODEX_TOOLS.has(tool)) return "codex";
2534
2574
  if (ZCODE_TOOLS.has(tool)) return "zcode";
2535
2575
  return CURSOR_TOOLS.has(tool) ? "cursor" : "claude";
@@ -2619,6 +2659,20 @@ function toCodexEntry(def) {
2619
2659
  if (def.matcher && def.matcher !== "*") entry.matcher = def.matcher;
2620
2660
  return entry;
2621
2661
  }
2662
+ function copilotPowershellCommand(command) {
2663
+ const match = command.match(COPILOT_BUILTIN_COMMAND_RE);
2664
+ return match ? `${match[1]} 2>$null; exit 0` : command;
2665
+ }
2666
+ function toCopilotEntry(def) {
2667
+ return {
2668
+ type: "command",
2669
+ bash: def.command,
2670
+ powershell: copilotPowershellCommand(def.command),
2671
+ command: def.command,
2672
+ ...def.matcher && def.matcher !== "*" ? { matcher: def.matcher } : {},
2673
+ ...def.timeout !== void 0 ? { timeoutSec: def.timeout } : {}
2674
+ };
2675
+ }
2622
2676
  function toZcodeEntry(def) {
2623
2677
  const ZCODE_TIMEOUT_MS = {
2624
2678
  SessionStart: 18e4,
@@ -2626,6 +2680,7 @@ function toZcodeEntry(def) {
2626
2680
  PostToolUse: 3e4,
2627
2681
  UserPromptSubmit: 6e4
2628
2682
  };
2683
+ const timeoutMs = def.timeout !== void 0 ? def.timeout * 1e3 : ZCODE_TIMEOUT_MS[def.event] ?? 6e4;
2629
2684
  const entry = process.platform === "win32" ? {
2630
2685
  // Windows must NOT spawn bare `bash`: CreateProcess resolves it to
2631
2686
  // System32's WSL launcher before any PATH directory, and the WSL side
@@ -2635,7 +2690,7 @@ function toZcodeEntry(def) {
2635
2690
  type: "process",
2636
2691
  command: "cmd",
2637
2692
  args: ["/c", def.command],
2638
- timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 6e4
2693
+ timeoutMs
2639
2694
  } : {
2640
2695
  type: "process",
2641
2696
  command: "bash",
@@ -2643,7 +2698,7 @@ function toZcodeEntry(def) {
2643
2698
  // so managed-entry detection and the managed-hooks manifest share one
2644
2699
  // command representation (the same invariant the Codex format keeps).
2645
2700
  args: ["-lc", def.command],
2646
- timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 6e4
2701
+ timeoutMs
2647
2702
  };
2648
2703
  const group = { hooks: [entry] };
2649
2704
  if (def.matcher && def.matcher !== "*") group.matcher = def.matcher;
@@ -2763,6 +2818,61 @@ async function reconcileCursorFormat(hooksPath, tool, teamDefs, opts, priorTeamC
2763
2818
  log.debug(`teamai hooks already up-to-date in ${hooksPath}`);
2764
2819
  }
2765
2820
  }
2821
+ function copilotEntryCommands(entry) {
2822
+ return [entry.bash, entry.powershell, entry.command].filter(Boolean);
2823
+ }
2824
+ async function reconcileCopilotFormat(hooksPath, tool, teamDefs, opts, priorTeamCommands) {
2825
+ const expanded = expandHome(hooksPath);
2826
+ const existed = await pathExists(expanded);
2827
+ if (opts.removeAll && !existed) {
2828
+ log.debug(`No teamai hooks to remove from ${hooksPath}`);
2829
+ return;
2830
+ }
2831
+ await ensureDir(path14.dirname(expanded));
2832
+ const hooksJson = await readJson(expanded) ?? {
2833
+ version: COPILOT_HOOK_SCHEMA_VERSION,
2834
+ hooks: {}
2835
+ };
2836
+ let changed = hooksJson.version !== COPILOT_HOOK_SCHEMA_VERSION;
2837
+ hooksJson.version = COPILOT_HOOK_SCHEMA_VERSION;
2838
+ if (!hooksJson.hooks) hooksJson.hooks = {};
2839
+ const isManaged = (entry) => copilotEntryCommands(entry).some(
2840
+ (command) => TEAMAI_COMMAND_MARKERS.some((marker) => command.includes(marker)) || priorTeamCommands.has(command)
2841
+ );
2842
+ const defs = opts.removeAll ? [] : desiredDefs(tool, teamDefs, opts.builtinOverride);
2843
+ const desiredByEvent = {};
2844
+ for (const def of defs) {
2845
+ const event = CLAUDE_TO_COPILOT_EVENTS[def.event];
2846
+ if (!event) continue;
2847
+ (desiredByEvent[event] ??= []).push(toCopilotEntry(def));
2848
+ }
2849
+ for (const event of Object.keys(hooksJson.hooks)) {
2850
+ const existing = hooksJson.hooks[event] ?? [];
2851
+ const untouched = existing.filter((entry) => !isManaged(entry));
2852
+ const desired = desiredByEvent[event] ?? [];
2853
+ const next = [...untouched, ...desired];
2854
+ if (next.length === 0 && !opts.removeAll) {
2855
+ if (existing.length > 0) changed = true;
2856
+ delete hooksJson.hooks[event];
2857
+ continue;
2858
+ }
2859
+ if (JSON.stringify(existing) !== JSON.stringify(next)) {
2860
+ hooksJson.hooks[event] = next;
2861
+ changed = true;
2862
+ }
2863
+ }
2864
+ for (const event of desiredEventOrder(defs, (value) => CLAUDE_TO_COPILOT_EVENTS[value])) {
2865
+ if (hooksJson.hooks[event]) continue;
2866
+ hooksJson.hooks[event] = desiredByEvent[event];
2867
+ changed = true;
2868
+ }
2869
+ if (changed || !existed) {
2870
+ await writeJson(expanded, hooksJson);
2871
+ log.success(`${opts.removeAll ? "Removed" : "Updated"} teamai hooks in ${hooksPath}`);
2872
+ } else {
2873
+ log.debug(`teamai hooks already up-to-date in ${hooksPath}`);
2874
+ }
2875
+ }
2766
2876
  async function reconcileCodexFormat(hooksPath, tool, teamDefs, opts, priorTeamCommands) {
2767
2877
  const expanded = expandHome(hooksPath);
2768
2878
  await ensureDir(path14.dirname(expanded));
@@ -2949,6 +3059,8 @@ async function reconcileHooks(settingsPath, tool, teamDefs = [], opts = {}) {
2949
3059
  const format = detectFormat(tool);
2950
3060
  if (format === "cursor") {
2951
3061
  await reconcileCursorFormat(settingsPath, tool, scopedDefs, opts, priorTeamCommands);
3062
+ } else if (format === "copilot") {
3063
+ await reconcileCopilotFormat(settingsPath, tool, scopedDefs, opts, priorTeamCommands);
2952
3064
  } else if (format === "codex") {
2953
3065
  await reconcileCodexFormat(settingsPath, tool, scopedDefs, opts, priorTeamCommands);
2954
3066
  } else if (format === "zcode") {
@@ -3002,6 +3114,19 @@ async function getHookStatus(settingsPath, tool) {
3002
3114
  });
3003
3115
  return present2 ? "installed" : "missing";
3004
3116
  }
3117
+ if (format === "copilot") {
3118
+ const hooksJson = await readJson(expanded);
3119
+ if (hooksJson?.version !== COPILOT_HOOK_SCHEMA_VERSION || !hooksJson.hooks) return "missing";
3120
+ const present2 = defs.every((def) => {
3121
+ const event = CLAUDE_TO_COPILOT_EVENTS[def.event];
3122
+ if (!event) return true;
3123
+ const want = toCopilotEntry(def);
3124
+ return (hooksJson.hooks[event] ?? []).some(
3125
+ (entry) => entry.type === want.type && entry.bash === want.bash && entry.powershell === want.powershell && entry.command === want.command && entry.matcher === want.matcher
3126
+ );
3127
+ });
3128
+ return present2 ? "installed" : "missing";
3129
+ }
3005
3130
  if (format === "codex") {
3006
3131
  const hooksJson = await readJson(expanded);
3007
3132
  if (!hooksJson?.hooks) return "missing";
@@ -3045,6 +3170,15 @@ async function hasTeamaiHooks(settingsPath, tool, manifestPath) {
3045
3170
  (entries) => (entries ?? []).some((e) => isTeamaiHookCommand(e.command) || priorTeamCommands.has(e.command))
3046
3171
  );
3047
3172
  }
3173
+ if (format === "copilot") {
3174
+ const j = await readJson(expanded);
3175
+ if (!j?.hooks) return false;
3176
+ return Object.values(j.hooks).some(
3177
+ (entries) => (entries ?? []).some((entry) => copilotEntryCommands(entry).some(
3178
+ (command) => TEAMAI_COMMAND_MARKERS.some((marker) => command.includes(marker)) || priorTeamCommands.has(command)
3179
+ ))
3180
+ );
3181
+ }
3048
3182
  if (format === "codex") {
3049
3183
  const j = await readJson(expanded);
3050
3184
  if (!j?.hooks) return false;
@@ -3213,7 +3347,8 @@ async function reconcileTeamHooksForConfig(teamConfig, localConfig, opts = {}) {
3213
3347
  activeRoles: activeRoleIds(localConfig)
3214
3348
  });
3215
3349
  const { baseDir, manifestPath } = resolveHookScope(localConfig);
3216
- let filterAgents2 = opts.filterAgents ?? localConfig.enabledAgents;
3350
+ const explicitlySelectedAgents = opts.filterAgents ?? localConfig.enabledAgents;
3351
+ let filterAgents2 = explicitlySelectedAgents;
3217
3352
  const disabled = localConfig.disabledAgents;
3218
3353
  if (disabled && disabled.length > 0) {
3219
3354
  const universe = filterAgents2 ?? Object.keys(teamConfig.toolPaths);
@@ -3226,10 +3361,29 @@ async function reconcileTeamHooksForConfig(teamConfig, localConfig, opts = {}) {
3226
3361
  teamHookProjectRoot: localConfig.scope === "project" && !isSelfMode(localConfig) ? localConfig.projectRoot : void 0,
3227
3362
  installedBaseDir: localConfig.scope === "project" ? localConfig.projectRoot ?? baseDir : void 0
3228
3363
  });
3364
+ const copilotExcluded = disabled?.includes(COPILOT_TOOL_ID) ?? false;
3365
+ const copilotSelected = !copilotExcluded && (explicitlySelectedAgents?.includes(COPILOT_TOOL_ID) ?? false);
3366
+ const copilotEnabled = !copilotExcluded && (copilotSelected || explicitlySelectedAgents === void 0 && await pathExists(getCopilotHome()));
3367
+ const copilotPaths = scopedToolPaths(teamConfig, localConfig)[COPILOT_TOOL_ID];
3368
+ if (copilotEnabled && copilotPaths?.hooks) {
3369
+ const copilotBase = resolveToolBaseDir(COPILOT_TOOL_ID, localConfig);
3370
+ if (copilotSelected || await pathExists(getCopilotHome())) {
3371
+ await reconcileHooks(
3372
+ path14.join(copilotBase, copilotPaths.hooks),
3373
+ COPILOT_TOOL_ID,
3374
+ teamDefs,
3375
+ {
3376
+ manifestPath: getManagedHooksPath(localConfig.scope, localConfig.projectRoot),
3377
+ removeAll: opts.removeAll,
3378
+ builtinOverride: builtin
3379
+ }
3380
+ );
3381
+ }
3382
+ }
3229
3383
  await sweepLegacyProjectHooks(teamConfig.toolPaths, localConfig);
3230
3384
  return teamDefs;
3231
3385
  }
3232
- var OPENCLAW_TOOLS, TEAMAI_HOOK_SUBCOMMANDS, TEAMAI_LEGACY_HOOK_SUBCOMMANDS, CLAUDE_TO_CURSOR_EVENTS, CURSOR_TOOLS, CODEX_TOOLS, ZCODE_TOOLS, CODEX_TRUST_GATE_TOOLS, TEAMAI_COMMAND_MARKERS, AGENT_HOOK_EVENTS;
3386
+ var OPENCLAW_TOOLS, TEAMAI_HOOK_SUBCOMMANDS, TEAMAI_LEGACY_HOOK_SUBCOMMANDS, CLAUDE_TO_CURSOR_EVENTS, CLAUDE_TO_COPILOT_EVENTS, COPILOT_HOOK_SCHEMA_VERSION, CURSOR_TOOLS, CODEX_TOOLS, ZCODE_TOOLS, CODEX_TRUST_GATE_TOOLS, TEAMAI_COMMAND_MARKERS, COPILOT_BUILTIN_COMMAND_RE, AGENT_HOOK_EVENTS;
3233
3387
  var init_hooks2 = __esm({
3234
3388
  "src/hooks.ts"() {
3235
3389
  "use strict";
@@ -3250,6 +3404,14 @@ var init_hooks2 = __esm({
3250
3404
  PostToolUse: "postToolUse",
3251
3405
  UserPromptSubmit: "beforeSubmitPrompt"
3252
3406
  };
3407
+ CLAUDE_TO_COPILOT_EVENTS = {
3408
+ SessionStart: "SessionStart",
3409
+ Stop: "Stop",
3410
+ UserPromptSubmit: "UserPromptSubmit",
3411
+ PreToolUse: "PreToolUse",
3412
+ PostToolUse: "PostToolUse"
3413
+ };
3414
+ COPILOT_HOOK_SCHEMA_VERSION = 1;
3253
3415
  CURSOR_TOOLS = /* @__PURE__ */ new Set(["cursor"]);
3254
3416
  CODEX_TOOLS = /* @__PURE__ */ new Set(["codex", "codex-internal", "tcodex"]);
3255
3417
  ZCODE_TOOLS = /* @__PURE__ */ new Set(["zcode"]);
@@ -3265,6 +3427,7 @@ var init_hooks2 = __esm({
3265
3427
  "teamai mr-hint",
3266
3428
  "teamai hook-dispatch"
3267
3429
  ];
3430
+ COPILOT_BUILTIN_COMMAND_RE = /^bash -lc "(teamai hook-dispatch [^"]+) 2>\/dev\/null" \|\| true$/;
3268
3431
  AGENT_HOOK_EVENTS = /* @__PURE__ */ new Set([
3269
3432
  "SessionStart",
3270
3433
  "UserPromptSubmit",
@@ -3370,12 +3533,17 @@ var init_pending_push = __esm({
3370
3533
 
3371
3534
  // src/resources/rule-format.ts
3372
3535
  function ruleFileExtensionForTool(tool) {
3373
- return usesCursorMdcRules(tool) ? ".mdc" : ".md";
3536
+ if (usesCursorMdcRules(tool)) return ".mdc";
3537
+ return usesCopilotInstructions(tool) ? ".instructions.md" : ".md";
3374
3538
  }
3375
3539
  function usesCursorMdcRules(tool) {
3376
3540
  return CURSOR_MDC_RULE_TOOLS.has(tool);
3377
3541
  }
3542
+ function usesCopilotInstructions(tool) {
3543
+ return COPILOT_INSTRUCTIONS_RULE_TOOLS.has(tool);
3544
+ }
3378
3545
  function ruleStemFromFilename(filename) {
3546
+ if (filename.endsWith(".instructions.md")) return filename.slice(0, -".instructions.md".length);
3379
3547
  if (filename.endsWith(".mdc")) return filename.slice(0, -".mdc".length);
3380
3548
  if (filename.endsWith(".md")) return filename.slice(0, -".md".length);
3381
3549
  return null;
@@ -3383,11 +3551,12 @@ function ruleStemFromFilename(filename) {
3383
3551
  function isLegacyCursorRuleFile(tool, filename) {
3384
3552
  return usesCursorMdcRules(tool) && filename.endsWith(".md");
3385
3553
  }
3386
- var CURSOR_MDC_RULE_TOOLS;
3554
+ var CURSOR_MDC_RULE_TOOLS, COPILOT_INSTRUCTIONS_RULE_TOOLS;
3387
3555
  var init_rule_format = __esm({
3388
3556
  "src/resources/rule-format.ts"() {
3389
3557
  "use strict";
3390
3558
  CURSOR_MDC_RULE_TOOLS = /* @__PURE__ */ new Set(["cursor", "joycode"]);
3559
+ COPILOT_INSTRUCTIONS_RULE_TOOLS = /* @__PURE__ */ new Set(["copilot"]);
3391
3560
  }
3392
3561
  });
3393
3562
 
@@ -8058,20 +8227,73 @@ var init_dashboard_collector = __esm({
8058
8227
  }
8059
8228
  });
8060
8229
 
8230
+ // src/utils/exec.ts
8231
+ import spawn from "cross-spawn";
8232
+ async function probeBinary(binary, args = ["--version"], executor = execCommand) {
8233
+ try {
8234
+ const result = await executor(binary, args, { timeoutMs: 5e3 });
8235
+ if (result.code !== 0) return "";
8236
+ return (result.stdout || result.stderr).trim();
8237
+ } catch {
8238
+ return "";
8239
+ }
8240
+ }
8241
+ function formatCommand(command, args) {
8242
+ const quote = (value) => /^[a-zA-Z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value);
8243
+ return [command, ...args.map(quote)].join(" ");
8244
+ }
8245
+ var execCommand;
8246
+ var init_exec = __esm({
8247
+ "src/utils/exec.ts"() {
8248
+ "use strict";
8249
+ execCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
8250
+ const child = spawn(command, args, {
8251
+ cwd: options.cwd,
8252
+ env: options.env,
8253
+ stdio: ["ignore", "pipe", "pipe"],
8254
+ shell: false
8255
+ });
8256
+ let stdout = "";
8257
+ let stderr = "";
8258
+ let settled = false;
8259
+ const timeoutMs = options.timeoutMs ?? 12e4;
8260
+ const finish = (callback) => {
8261
+ if (settled) return;
8262
+ settled = true;
8263
+ clearTimeout(timer);
8264
+ callback();
8265
+ };
8266
+ child.stdout?.on("data", (chunk) => {
8267
+ const text = chunk.toString();
8268
+ stdout += text;
8269
+ if (options.stream) process.stdout.write(text);
8270
+ });
8271
+ child.stderr?.on("data", (chunk) => {
8272
+ const text = chunk.toString();
8273
+ stderr += text;
8274
+ if (options.stream) process.stderr.write(text);
8275
+ });
8276
+ const timer = setTimeout(() => {
8277
+ child.kill();
8278
+ finish(() => reject(new Error(
8279
+ `Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`
8280
+ )));
8281
+ }, timeoutMs);
8282
+ child.on("error", (error) => finish(() => reject(error)));
8283
+ child.on("close", (code) => finish(() => resolve({
8284
+ stdout,
8285
+ stderr,
8286
+ code: code ?? 1
8287
+ })));
8288
+ });
8289
+ }
8290
+ });
8291
+
8061
8292
  // src/agent-version.ts
8062
- import { execFile } from "child_process";
8063
8293
  import { readFile } from "fs/promises";
8064
8294
  import path22 from "path";
8065
8295
  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
- });
8296
+ return probeBinary(bin, args);
8075
8297
  }
8076
8298
  async function readPlistVersion(appPath) {
8077
8299
  const plistPath = path22.join(appPath, "Contents", "Info.plist");
@@ -8145,6 +8367,7 @@ var init_agent_version = __esm({
8145
8367
  "src/agent-version.ts"() {
8146
8368
  "use strict";
8147
8369
  init_logger();
8370
+ init_exec();
8148
8371
  VERSION_CACHE = /* @__PURE__ */ new Map();
8149
8372
  CODEBUDDY_IDE_PATHS = [
8150
8373
  "/Applications/CodeBuddy.app",
@@ -8805,6 +9028,10 @@ function codexServerNames(source) {
8805
9028
  for (const m of source.matchAll(/^\[mcp_servers\.([A-Za-z0-9_-]+)\]\s*$/gm)) names.add(m[1]);
8806
9029
  return [...names];
8807
9030
  }
9031
+ function mcpTargetExcluded(localConfig, target) {
9032
+ if (!isAgentExcluded(localConfig, target.tool)) return false;
9033
+ return !(target.projectScope && target.tool === "claude" && !isAgentExcluded(localConfig, "tclaude"));
9034
+ }
8808
9035
  async function reconcileMcpForConfig(teamConfig, localConfig, options = {}) {
8809
9036
  const changes = [];
8810
9037
  let wrote = false;
@@ -8843,6 +9070,7 @@ async function reconcileMcpForConfig(teamConfig, localConfig, options = {}) {
8843
9070
  if (teamDefs.length === 0 && nothingOwned) return { changes, wrote };
8844
9071
  const vars = await buildVarTable(localConfig);
8845
9072
  for (const target of targets) {
9073
+ if (!removeAll && mcpTargetExcluded(localConfig, target)) continue;
8846
9074
  const manifestKey = managedMcpManifestKey(target.tool, target.projectScope);
8847
9075
  const owned = manifest[manifestKey] ?? [];
8848
9076
  const ownedNames = new Set(owned.map((r) => r.name));
@@ -9305,7 +9533,7 @@ __export(local_agent_exports, {
9305
9533
  import fs14 from "fs";
9306
9534
  import os7 from "os";
9307
9535
  import path28 from "path";
9308
- import { execFile as execFile2 } from "child_process";
9536
+ import { execFile } from "child_process";
9309
9537
  import { promisify } from "util";
9310
9538
  import fse4 from "fs-extra";
9311
9539
  function modelAgentKind(tool) {
@@ -11544,7 +11772,7 @@ var init_local_agent = __esm({
11544
11772
  init_types();
11545
11773
  init_home();
11546
11774
  init_git2();
11547
- execFileAsync = promisify(execFile2);
11775
+ execFileAsync = promisify(execFile);
11548
11776
  LOCAL_AGENT_DIR = "local-agent";
11549
11777
  CONFIG_FILE = "config.json";
11550
11778
  MANIFEST_FILE = "manifest.json";
@@ -12272,9 +12500,9 @@ var init_skills = __esm({
12272
12500
  sourceSkillNames = /* @__PURE__ */ new Set();
12273
12501
  }
12274
12502
  const candidates = /* @__PURE__ */ new Map();
12275
- for (const [_tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12503
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12276
12504
  if (!toolPath.skills) continue;
12277
- const skillsDir = path30.join(resolveBaseDir(localConfig), toolPath.skills);
12505
+ const skillsDir = path30.join(resolveToolBaseDir(tool, localConfig), toolPath.skills);
12278
12506
  if (!await pathExists(skillsDir)) continue;
12279
12507
  const localSkills = await scanSkillsRecursively(skillsDir);
12280
12508
  for (const [dir, localDirPath] of localSkills) {
@@ -12382,7 +12610,6 @@ var init_skills = __esm({
12382
12610
  * Pull a skill from team repo to all configured AI tool directories.
12383
12611
  */
12384
12612
  async pullItem(item, teamConfig, localConfig) {
12385
- const baseDir = resolveBaseDir(localConfig);
12386
12613
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12387
12614
  if (isAgentExcluded(localConfig, tool)) continue;
12388
12615
  if (!toolPath.skills) continue;
@@ -12401,10 +12628,11 @@ var init_skills = __esm({
12401
12628
  }
12402
12629
  dest = path30.join(getHermesHome(), "skills", item.name);
12403
12630
  } else {
12404
- if (!await ResourceHandler.isToolInstalled(toolPath.skills, baseDir)) {
12631
+ if (!await isToolInstalledForConfig(tool, toolPath.skills, localConfig)) {
12405
12632
  log.debug(`Skipping skill sync for ${tool}: tool not installed`);
12406
12633
  continue;
12407
12634
  }
12635
+ const baseDir = resolveToolBaseDir(tool, localConfig);
12408
12636
  dest = await resolveSkillDestination(tool, toolPath.skills, baseDir, item.name, item.sourcePath);
12409
12637
  }
12410
12638
  try {
@@ -12421,7 +12649,6 @@ var init_skills = __esm({
12421
12649
  */
12422
12650
  async removeItem(name, teamConfig, localConfig) {
12423
12651
  const removed = [];
12424
- const baseDir = resolveBaseDir(localConfig);
12425
12652
  const scopedNamespaces = await resolveSkillNamespaces(localConfig);
12426
12653
  if (scopedNamespaces.length > 0) {
12427
12654
  for (const namespace of scopedNamespaces) {
@@ -12441,12 +12668,14 @@ var init_skills = __esm({
12441
12668
  await this.addTombstone(name, localConfig);
12442
12669
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12443
12670
  if (!toolPath.skills) continue;
12671
+ if (isAgentExcluded(localConfig, tool)) continue;
12444
12672
  let skillDir;
12445
12673
  if (tool === "openclaw") {
12446
12674
  const wsDir = await resolveOpenclawWorkspaceDir();
12447
12675
  if (!wsDir) continue;
12448
12676
  skillDir = path30.join(wsDir, "skills", name);
12449
12677
  } else {
12678
+ const baseDir = resolveToolBaseDir(tool, localConfig);
12450
12679
  const configuredDir = path30.join(baseDir, toolPath.skills, name);
12451
12680
  skillDir = await resolveSkillDestination(tool, toolPath.skills, baseDir, name);
12452
12681
  if (skillDir !== configuredDir && await pathExists(configuredDir) && await dirContentEqual(skillDir, configuredDir)) {
@@ -12475,6 +12704,55 @@ var init_skills = __esm({
12475
12704
  }
12476
12705
  });
12477
12706
 
12707
+ // src/resources/copilot-instructions.ts
12708
+ function normalizeBody2(body) {
12709
+ return body.replace(/^\s+/, "").replace(/\s+$/, "");
12710
+ }
12711
+ function rulePaths(data) {
12712
+ const value = data.paths;
12713
+ if (Array.isArray(value)) {
12714
+ return value.map((entry) => String(entry).trim()).filter(Boolean);
12715
+ }
12716
+ if (typeof value === "string") {
12717
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
12718
+ }
12719
+ return [];
12720
+ }
12721
+ function teamRuleToCopilotInstructions(rawTeamRule) {
12722
+ const { data, body } = splitFrontmatter2(rawTeamRule);
12723
+ const paths = rulePaths(data);
12724
+ return stringifyFrontmatter(
12725
+ { applyTo: paths.length > 0 ? paths.join(", ") : ALL_FILES_GLOB },
12726
+ `
12727
+ ${normalizeBody2(body)}
12728
+ `
12729
+ );
12730
+ }
12731
+ function mergeCopilotBodyIntoTeamMd(rawCopilotInstructions, existingTeamMd) {
12732
+ const body = normalizeBody2(splitFrontmatter2(rawCopilotInstructions).body);
12733
+ if (existingTeamMd === null) return `${body}
12734
+ `;
12735
+ const existing = splitFrontmatter2(existingTeamMd);
12736
+ if (normalizeBody2(existing.body) === body) return existingTeamMd;
12737
+ if (!existing.raw) return `${body}
12738
+ `;
12739
+ return `${existing.raw.endsWith("\n") ? existing.raw : `${existing.raw}
12740
+ `}
12741
+ ${body}
12742
+ `;
12743
+ }
12744
+ function copilotInstructionsBodyEqualsTeamMd(rawCopilotInstructions, rawTeamRule) {
12745
+ return normalizeBody2(splitFrontmatter2(rawCopilotInstructions).body) === normalizeBody2(splitFrontmatter2(rawTeamRule).body);
12746
+ }
12747
+ var ALL_FILES_GLOB;
12748
+ var init_copilot_instructions = __esm({
12749
+ "src/resources/copilot-instructions.ts"() {
12750
+ "use strict";
12751
+ init_frontmatter();
12752
+ ALL_FILES_GLOB = "**";
12753
+ }
12754
+ });
12755
+
12478
12756
  // src/resources/opencode-config.ts
12479
12757
  var opencode_config_exports = {};
12480
12758
  __export(opencode_config_exports, {
@@ -12547,6 +12825,7 @@ var init_rules = __esm({
12547
12825
  init_types();
12548
12826
  init_builtin_rules();
12549
12827
  init_cursor_mdc();
12828
+ init_copilot_instructions();
12550
12829
  init_rule_format();
12551
12830
  RulesHandler = class extends ResourceHandler {
12552
12831
  type = "rules";
@@ -12574,10 +12853,11 @@ var init_rules = __esm({
12574
12853
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12575
12854
  const rulesPath = toolPath.rules;
12576
12855
  if (!rulesPath) continue;
12577
- const rulesDir = path32.join(resolveBaseDir(localConfig), rulesPath);
12856
+ const rulesDir = path32.join(resolveToolBaseDir(tool, localConfig), rulesPath);
12578
12857
  if (!await pathExists(rulesDir)) continue;
12579
12858
  const ext = ruleFileExtensionForTool(tool);
12580
12859
  const isMdcTool = usesCursorMdcRules(tool);
12860
+ const isCopilotTool = usesCopilotInstructions(tool);
12581
12861
  const files = await listFilesRecursive(rulesDir);
12582
12862
  for (const file of files) {
12583
12863
  if (!file.endsWith(ext)) continue;
@@ -12588,10 +12868,12 @@ var init_rules = __esm({
12588
12868
  const teamFileName = `${name}.md`;
12589
12869
  if (teamRules.has(teamFileName)) {
12590
12870
  const teamFilePath = path32.join(teamRulesDir, teamFileName);
12871
+ const localRule = await readFileSafe(localFilePath) ?? "";
12872
+ const teamRule = await readTeamRule(teamFilePath);
12591
12873
  const equal = isMdcTool ? cursorMdcBodyEqualsTeamMd(
12592
- await readFileSafe(localFilePath) ?? "",
12593
- await readTeamRule(teamFilePath)
12594
- ) : await fileContentEqual(localFilePath, teamFilePath);
12874
+ localRule,
12875
+ teamRule
12876
+ ) : isCopilotTool ? copilotInstructionsBodyEqualsTeamMd(localRule, teamRule) : await fileContentEqual(localFilePath, teamFilePath);
12595
12877
  if (equal) continue;
12596
12878
  const mtime = await getFileMtime(localFilePath);
12597
12879
  const existing = candidates.get(name);
@@ -12599,7 +12881,7 @@ var init_rules = __esm({
12599
12881
  candidates.set(name, { sourcePath: localFilePath, mtime, status: "modified" });
12600
12882
  }
12601
12883
  } else {
12602
- if (isMdcTool) continue;
12884
+ if (isMdcTool || isCopilotTool) continue;
12603
12885
  const existing = candidates.get(name);
12604
12886
  if (!existing) {
12605
12887
  const mtime = await getFileMtime(localFilePath);
@@ -12645,6 +12927,12 @@ var init_rules = __esm({
12645
12927
  throw new Error(`Cannot read rule source ${item.sourcePath}`);
12646
12928
  }
12647
12929
  await writeFile(dest, mergeCursorBodyIntoTeamMd(raw, await readFileSafe(dest)));
12930
+ } else if (item.sourcePath.endsWith(".instructions.md")) {
12931
+ const raw = await readFileSafe(item.sourcePath);
12932
+ if (raw === null) {
12933
+ throw new Error(`Cannot read rule source ${item.sourcePath}`);
12934
+ }
12935
+ await writeFile(dest, mergeCopilotBodyIntoTeamMd(raw, await readFileSafe(dest)));
12648
12936
  } else {
12649
12937
  await copyFile(item.sourcePath, dest);
12650
12938
  }
@@ -12655,15 +12943,14 @@ var init_rules = __esm({
12655
12943
  * Pull a single rule file to all configured AI tool rules/ directories.
12656
12944
  */
12657
12945
  async pullItem(item, teamConfig, localConfig) {
12658
- const baseDir = resolveBaseDir(localConfig);
12659
12946
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12660
12947
  if (isAgentExcluded(localConfig, tool)) continue;
12661
12948
  if (!toolPath.rules) continue;
12662
- if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) {
12949
+ if (!await isToolInstalledForConfig(tool, toolPath.rules, localConfig)) {
12663
12950
  log.debug(`Skipping rule sync for ${tool}: tool not installed`);
12664
12951
  continue;
12665
12952
  }
12666
- const destDir = path32.join(baseDir, toolPath.rules);
12953
+ const destDir = path32.join(resolveToolBaseDir(tool, localConfig), toolPath.rules);
12667
12954
  await ensureDir(destDir);
12668
12955
  const dest = path32.join(destDir, `${item.name}${ruleFileExtensionForTool(tool)}`);
12669
12956
  try {
@@ -12674,6 +12961,13 @@ var init_rules = __esm({
12674
12961
  }
12675
12962
  await writeFile(dest, teamRuleToCursorMdc(raw));
12676
12963
  await remove(path32.join(destDir, `${item.name}.md`));
12964
+ } else if (usesCopilotInstructions(tool)) {
12965
+ const raw = await readFileSafe(item.sourcePath);
12966
+ if (raw === null) {
12967
+ throw new Error(`Cannot read rule source ${item.sourcePath}`);
12968
+ }
12969
+ await writeFile(dest, teamRuleToCopilotInstructions(raw));
12970
+ await remove(path32.join(destDir, `${item.name}.md`));
12677
12971
  } else {
12678
12972
  await copyFile(item.sourcePath, dest);
12679
12973
  }
@@ -12688,7 +12982,6 @@ var init_rules = __esm({
12688
12982
  */
12689
12983
  async removeItem(name, teamConfig, localConfig) {
12690
12984
  const removed = [];
12691
- const baseDir = resolveBaseDir(localConfig);
12692
12985
  const teamFile = path32.join(localConfig.repo.localPath, "rules", `${name}.md`);
12693
12986
  if (await pathExists(teamFile)) {
12694
12987
  await remove(teamFile);
@@ -12697,6 +12990,8 @@ var init_rules = __esm({
12697
12990
  await this.addTombstone(name, localConfig);
12698
12991
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12699
12992
  if (!toolPath.rules) continue;
12993
+ if (isAgentExcluded(localConfig, tool)) continue;
12994
+ const baseDir = resolveToolBaseDir(tool, localConfig);
12700
12995
  const extensions = /* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"]);
12701
12996
  for (const extension of extensions) {
12702
12997
  const filePath = path32.join(baseDir, toolPath.rules, `${name}${extension}`);
@@ -12735,10 +13030,11 @@ var init_rules = __esm({
12735
13030
  }
12736
13031
  const teamRuleNames = new Set(rules.map((r) => r.name));
12737
13032
  const tombstones = await this.readTombstones(localConfig);
12738
- const baseDir = resolveBaseDir(localConfig);
12739
13033
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12740
13034
  if (!toolPath.rules) continue;
12741
- if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
13035
+ if (isAgentExcluded(localConfig, tool)) continue;
13036
+ if (!await isToolInstalledForConfig(tool, toolPath.rules, localConfig)) continue;
13037
+ const baseDir = resolveToolBaseDir(tool, localConfig);
12742
13038
  const destDir = path32.join(baseDir, toolPath.rules);
12743
13039
  if (!await pathExists(destDir)) continue;
12744
13040
  const ext = ruleFileExtensionForTool(tool);
@@ -12746,7 +13042,7 @@ var init_rules = __esm({
12746
13042
  for (const localFile of localFiles) {
12747
13043
  const ruleName = ruleStemFromFilename(localFile);
12748
13044
  if (ruleName === null) continue;
12749
- if (tool === "joycode" && !tombstones.has(ruleName)) continue;
13045
+ if ((tool === "joycode" || usesCopilotInstructions(tool)) && !tombstones.has(ruleName)) continue;
12750
13046
  if (isLegacyCursorRuleFile(tool, localFile)) {
12751
13047
  await remove(path32.join(destDir, localFile));
12752
13048
  log.debug(`Removed legacy .md rule ${localFile} from ${tool}`);
@@ -12762,8 +13058,9 @@ var init_rules = __esm({
12762
13058
  }
12763
13059
  await this.removeEmptyDirs(destDir);
12764
13060
  }
12765
- for (const [, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13061
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12766
13062
  if (!toolPath.claudemd) continue;
13063
+ const baseDir = resolveToolBaseDir(tool, localConfig);
12767
13064
  const claudeMdPath = path32.join(baseDir, toolPath.claudemd);
12768
13065
  try {
12769
13066
  const content = await readFileSafe(claudeMdPath);
@@ -12941,9 +13238,9 @@ var init_env = __esm({
12941
13238
  }
12942
13239
  const envYamlPath = path34.join(localConfig.repo.localPath, "env", "env.yaml");
12943
13240
  if (!await pathExists(envYamlPath)) return [];
12944
- const { execFile: execFile5 } = await import("child_process");
13241
+ const { execFile: execFile4 } = await import("child_process");
12945
13242
  const { promisify: promisify4 } = await import("util");
12946
- const execFileAsync4 = promisify4(execFile5);
13243
+ const execFileAsync4 = promisify4(execFile4);
12947
13244
  try {
12948
13245
  await execFileAsync4("git", ["diff", "--exit-code", "env/env.yaml"], {
12949
13246
  cwd: localConfig.repo.localPath
@@ -13111,6 +13408,12 @@ import path35 from "path";
13111
13408
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
13112
13409
  import matter3 from "gray-matter";
13113
13410
  import { stringify as stringifyToml, parse as parseToml } from "smol-toml";
13411
+ function agentStemFromFilename(filename) {
13412
+ for (const ext of AGENT_FILE_EXTENSIONS) {
13413
+ if (filename.endsWith(ext)) return filename.slice(0, -ext.length);
13414
+ }
13415
+ return null;
13416
+ }
13114
13417
  function agentFileExtensionForTool(tool) {
13115
13418
  switch (tool) {
13116
13419
  case "codex":
@@ -13528,7 +13831,7 @@ function renderForTool(spec, tool) {
13528
13831
  return renderForOpencode(spec);
13529
13832
  }
13530
13833
  }
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;
13834
+ 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
13835
  var init_agent_format = __esm({
13533
13836
  "src/resources/agent-format.ts"() {
13534
13837
  "use strict";
@@ -13548,6 +13851,7 @@ var init_agent_format = __esm({
13548
13851
  "zcode",
13549
13852
  "opencode"
13550
13853
  ];
13854
+ AGENT_FILE_EXTENSIONS = [".md", ".toml", ".json"];
13551
13855
  KIRO_SESSION_START_COMMAND = getDispatchCommand("session-start", "kiro");
13552
13856
  COMMON_CLAUDE_FIELDS = /* @__PURE__ */ new Set(["name", "description", "model", "tools"]);
13553
13857
  COMMON_CURSOR_FIELDS = /* @__PURE__ */ new Set(["agent_id", "description", "model", "tools"]);
@@ -13585,8 +13889,7 @@ async function removeStaleAgentSiblings(targetAgentsDir, stem, targetExt) {
13585
13889
  return;
13586
13890
  }
13587
13891
  for (const file of files) {
13588
- const base = file.replace(/\.(md|toml|json)$/, "");
13589
- if (base !== stem) continue;
13892
+ if (agentStemFromFilename(file) !== stem) continue;
13590
13893
  if (file === `${stem}${targetExt}`) continue;
13591
13894
  try {
13592
13895
  await remove(path36.join(targetAgentsDir, file));
@@ -13669,10 +13972,267 @@ var init_builtin_agents = __esm({
13669
13972
  }
13670
13973
  });
13671
13974
 
13672
- // src/resources/agents.ts
13975
+ // src/projects.ts
13976
+ var projects_exports = {};
13977
+ __export(projects_exports, {
13978
+ describeProjects: () => describeProjects,
13979
+ findProject: () => findProject,
13980
+ isSafeNamespaceSegment: () => isSafeNamespaceSegment,
13981
+ listProjectIds: () => listProjectIds,
13982
+ loadProjectsManifest: () => loadProjectsManifest,
13983
+ mergeNamespaces: () => mergeNamespaces,
13984
+ resolveActiveLearningsNamespaces: () => resolveActiveLearningsNamespaces,
13985
+ resolveProjectResourceNamespaces: () => resolveProjectResourceNamespaces,
13986
+ saveProjectsManifest: () => saveProjectsManifest
13987
+ });
13673
13988
  import path37 from "path";
13989
+ import YAML8 from "yaml";
13990
+ import { z as z6 } from "zod";
13991
+ function isSafeNamespaceSegment(seg) {
13992
+ return SAFE_ID.test(seg) && seg !== "." && seg !== "..";
13993
+ }
13994
+ function validateManifestShape2(raw) {
13995
+ if (!raw || typeof raw !== "object") {
13996
+ throw new Error("Invalid projects manifest: expected an object");
13997
+ }
13998
+ const candidate = raw;
13999
+ const projects = candidate.projects;
14000
+ if (projects !== void 0 && !Array.isArray(projects)) {
14001
+ throw new Error("Invalid projects manifest: projects must be an array");
14002
+ }
14003
+ for (const project of projects ?? []) {
14004
+ if (!project || typeof project !== "object") {
14005
+ throw new Error("Invalid projects manifest: every project must be an object");
14006
+ }
14007
+ const resources = project.resources;
14008
+ if (resources !== void 0 && (typeof resources !== "object" || Array.isArray(resources))) {
14009
+ throw new Error(`Invalid projects manifest: project ${project.id ?? "<unknown>"} has invalid resources`);
14010
+ }
14011
+ if (resources) {
14012
+ const ALLOWED_RESOURCE_KEYS = new Set(PROJECT_RESOURCE_TYPES);
14013
+ for (const key of Object.keys(resources)) {
14014
+ if (!ALLOWED_RESOURCE_KEYS.has(key)) {
14015
+ throw new Error(`Invalid projects manifest: unknown resource type "${key}"`);
14016
+ }
14017
+ }
14018
+ }
14019
+ }
14020
+ const manifest = ProjectsManifestSchema.parse(raw);
14021
+ const ids = /* @__PURE__ */ new Set();
14022
+ for (const project of manifest.projects) {
14023
+ if (ids.has(project.id)) {
14024
+ throw new Error(`Invalid projects manifest: duplicate project id "${project.id}"`);
14025
+ }
14026
+ ids.add(project.id);
14027
+ }
14028
+ return manifest;
14029
+ }
14030
+ async function loadProjectsManifest(repoPath) {
14031
+ const manifestPath = path37.join(repoPath, "manifest", "projects.yaml");
14032
+ const content = await readFileSafe(manifestPath);
14033
+ if (!content) {
14034
+ return null;
14035
+ }
14036
+ let raw;
14037
+ try {
14038
+ raw = YAML8.parse(content);
14039
+ } catch (error) {
14040
+ throw new Error(`Invalid projects manifest YAML: ${error.message}`);
14041
+ }
14042
+ return validateManifestShape2(raw);
14043
+ }
14044
+ async function saveProjectsManifest(repoPath, manifest) {
14045
+ validateManifestShape2(manifest);
14046
+ const manifestDir = path37.join(repoPath, "manifest");
14047
+ const manifestPath = path37.join(manifestDir, "projects.yaml");
14048
+ await ensureDir(manifestDir);
14049
+ await writeFile(manifestPath, YAML8.stringify(manifest));
14050
+ }
14051
+ function findProject(manifest, projectId) {
14052
+ return manifest.projects.find((candidate) => candidate.id === projectId);
14053
+ }
14054
+ function listProjectIds(manifest) {
14055
+ return manifest.projects.map((project) => project.id);
14056
+ }
14057
+ function describeProjects(projects) {
14058
+ return projects.map((project) => {
14059
+ const label = project.name || project.id;
14060
+ return project.description ? `${label}: ${project.description}` : label;
14061
+ });
14062
+ }
14063
+ function getProjectOrThrow(manifest, projectId) {
14064
+ const project = findProject(manifest, projectId);
14065
+ if (!project) {
14066
+ throw new Error(`Unknown project "${projectId}". Valid projects: ${listProjectIds(manifest).join(", ")}`);
14067
+ }
14068
+ return project;
14069
+ }
14070
+ function resolveProjectResourceNamespaces(input) {
14071
+ const resolved = input.activeProjects.map((id) => getProjectOrThrow(input.manifest, id));
14072
+ const namespaces = {
14073
+ knowledge: [],
14074
+ skills: [],
14075
+ learnings: [],
14076
+ agents: []
14077
+ };
14078
+ for (const type of PROJECT_RESOURCE_TYPES) {
14079
+ const seen = /* @__PURE__ */ new Set();
14080
+ for (const project of resolved) {
14081
+ for (const namespace of project.resources[type]) {
14082
+ if (seen.has(namespace)) continue;
14083
+ seen.add(namespace);
14084
+ namespaces[type].push(namespace);
14085
+ }
14086
+ }
14087
+ }
14088
+ return namespaces;
14089
+ }
14090
+ async function resolveActiveLearningsNamespaces(repoPath, activeProjects) {
14091
+ if (activeProjects.length === 0) return [];
14092
+ const manifest = await loadProjectsManifest(repoPath);
14093
+ if (!manifest) return [];
14094
+ try {
14095
+ return resolveProjectResourceNamespaces({ manifest, activeProjects }).learnings;
14096
+ } catch {
14097
+ return [];
14098
+ }
14099
+ }
14100
+ function mergeNamespaces(roleNamespaces, projectNamespaces) {
14101
+ const dedupe = (a, b) => {
14102
+ const seen = /* @__PURE__ */ new Set();
14103
+ const out = [];
14104
+ for (const ns of [...a, ...b]) {
14105
+ if (seen.has(ns)) continue;
14106
+ seen.add(ns);
14107
+ out.push(ns);
14108
+ }
14109
+ return out;
14110
+ };
14111
+ return {
14112
+ knowledge: dedupe(roleNamespaces.knowledge, projectNamespaces.knowledge),
14113
+ skills: dedupe(roleNamespaces.skills, projectNamespaces.skills),
14114
+ // Roles never contribute learnings; this is effectively the project set.
14115
+ learnings: dedupe(roleNamespaces.learnings, projectNamespaces.learnings),
14116
+ agents: dedupe(roleNamespaces.agents, projectNamespaces.agents)
14117
+ };
14118
+ }
14119
+ var PROJECT_RESOURCE_TYPES, ProjectResourceNamespacesSchema, SAFE_ID, ProjectSchema, ProjectsManifestSchema;
14120
+ var init_projects = __esm({
14121
+ "src/projects.ts"() {
14122
+ "use strict";
14123
+ init_fs();
14124
+ PROJECT_RESOURCE_TYPES = ["knowledge", "skills", "learnings", "agents"];
14125
+ ProjectResourceNamespacesSchema = z6.object({
14126
+ knowledge: z6.array(z6.string().min(1)).default([]),
14127
+ skills: z6.array(z6.string().min(1)).default([]),
14128
+ learnings: z6.array(z6.string().min(1)).default([]),
14129
+ agents: z6.array(z6.string().min(1)).default([])
14130
+ });
14131
+ SAFE_ID = /^[A-Za-z0-9._-]+$/;
14132
+ ProjectSchema = z6.object({
14133
+ id: z6.string().min(1).refine((v) => isSafeNamespaceSegment(v), {
14134
+ message: "project id must be a single path segment (letters, digits, '.', '_', '-'; no '/', '\\\\', or '..')"
14135
+ }),
14136
+ name: z6.string().default(""),
14137
+ description: z6.string().default(""),
14138
+ resources: ProjectResourceNamespacesSchema
14139
+ });
14140
+ ProjectsManifestSchema = z6.object({
14141
+ version: z6.number(),
14142
+ // Unlike roles (`.min(1)`), a repo may define zero projects — a team without
14143
+ // project partitioning simply has no projects.yaml, and an empty list is valid.
14144
+ projects: z6.array(ProjectSchema).default([])
14145
+ });
14146
+ }
14147
+ });
14148
+
14149
+ // src/resource-namespaces.ts
14150
+ async function resolveResourceNamespaces(localConfig) {
14151
+ const activeProjects = localConfig.projects ?? [];
14152
+ const primaryRole = localConfig.primaryRole;
14153
+ const hasRole = !!primaryRole;
14154
+ const hasProjects = activeProjects.length > 0;
14155
+ const projectsManifest = await loadProjectsManifest(localConfig.repo.localPath);
14156
+ const teamHasProjects = !!projectsManifest && projectsManifest.projects.length > 0;
14157
+ if (!hasRole && !hasProjects && !teamHasProjects) return null;
14158
+ let roleNamespaces = { knowledge: [], skills: [], learnings: [], agents: [] };
14159
+ let allRoleSkillNamespaces = /* @__PURE__ */ new Set();
14160
+ if (primaryRole) {
14161
+ let rolesManifest;
14162
+ try {
14163
+ rolesManifest = await loadRolesManifest(localConfig.repo.localPath);
14164
+ } catch {
14165
+ log.warn("Could not load roles manifest. Skipping role-based filtering.");
14166
+ rolesManifest = null;
14167
+ }
14168
+ if (rolesManifest) {
14169
+ try {
14170
+ roleNamespaces = resolveRoleResourceNamespaces({
14171
+ manifest: rolesManifest,
14172
+ primaryRole,
14173
+ additionalRoles: localConfig.additionalRoles ?? []
14174
+ });
14175
+ allRoleSkillNamespaces = new Set(rolesManifest.roles.flatMap((role) => role.resources.skills));
14176
+ } catch {
14177
+ log.warn(`Role "${localConfig.primaryRole}" not found in manifest. Falling back to unfiltered sync.`);
14178
+ log.warn("Run `teamai roles set <role>` to pick a valid role.");
14179
+ if (!hasProjects && !teamHasProjects) return null;
14180
+ }
14181
+ } else if (!hasProjects && !teamHasProjects) {
14182
+ return null;
14183
+ }
14184
+ }
14185
+ let projectNamespaces = { knowledge: [], skills: [], learnings: [], agents: [] };
14186
+ let allProjectSkillNamespaces = /* @__PURE__ */ new Set();
14187
+ if (projectsManifest) {
14188
+ allProjectSkillNamespaces = new Set(projectsManifest.projects.flatMap((p) => p.resources.skills));
14189
+ if (hasProjects) {
14190
+ try {
14191
+ projectNamespaces = resolveProjectResourceNamespaces({
14192
+ manifest: projectsManifest,
14193
+ activeProjects
14194
+ });
14195
+ } catch (e) {
14196
+ log.warn(`${e instanceof Error ? e.message : String(e)} Falling back to role-only filtering.`);
14197
+ }
14198
+ }
14199
+ } else if (hasProjects) {
14200
+ log.warn("Active projects configured but no projects manifest found. Skipping project-based filtering.");
14201
+ }
14202
+ const activeNamespaces = mergeNamespaces(roleNamespaces, projectNamespaces);
14203
+ const allSkillNamespaces = /* @__PURE__ */ new Set([...allRoleSkillNamespaces, ...allProjectSkillNamespaces]);
14204
+ return { activeNamespaces, allSkillNamespaces };
14205
+ }
14206
+ var init_resource_namespaces = __esm({
14207
+ "src/resource-namespaces.ts"() {
14208
+ "use strict";
14209
+ init_roles();
14210
+ init_projects();
14211
+ init_logger();
14212
+ }
14213
+ });
14214
+
14215
+ // src/resources/agents.ts
14216
+ import path38 from "path";
13674
14217
  import { isDeepStrictEqual } from "util";
13675
14218
  import { parse as parseYaml2 } from "yaml";
14219
+ async function listTeamAgentDirs(teamAgentsDir) {
14220
+ const dirs = [{ dir: teamAgentsDir }];
14221
+ for (const namespace of await listDirs(teamAgentsDir)) {
14222
+ if (isSafeNamespaceSegment(namespace)) dirs.push({ dir: path38.join(teamAgentsDir, namespace), namespace });
14223
+ }
14224
+ return dirs;
14225
+ }
14226
+ async function findTeamAgentFiles(teamAgentsDir, stem) {
14227
+ const found = [];
14228
+ for (const { dir, namespace } of await listTeamAgentDirs(teamAgentsDir)) {
14229
+ for (const ext of [".yaml", ".md"]) {
14230
+ const candidate = path38.join(dir, `${stem}${ext}`);
14231
+ if (await pathExists(candidate)) found.push({ path: candidate, ext, ...namespace ? { namespace } : {} });
14232
+ }
14233
+ }
14234
+ return found;
14235
+ }
13676
14236
  function mergeCanonicalEdits(canonical, perTool) {
13677
14237
  const merged = { ...canonical };
13678
14238
  const extras = { ...canonical.tool_extras };
@@ -13714,16 +14274,10 @@ function mergeCanonicalEdits(canonical, perTool) {
13714
14274
  else delete merged.tool_extras;
13715
14275
  return { ok: true, spec: merged };
13716
14276
  }
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
14277
  async function removeStaleAgentSiblings2(agentsDir, stem, targetExt) {
13724
14278
  for (const file of await listFiles(agentsDir)) {
13725
- if (getAgentStem(file) !== stem || file === `${stem}${targetExt}`) continue;
13726
- await remove(path37.join(agentsDir, file));
14279
+ if (agentStemFromFilename(file) !== stem || file === `${stem}${targetExt}`) continue;
14280
+ await remove(path38.join(agentsDir, file));
13727
14281
  log.debug(`Removed stale agent sibling ${file} for ${stem}`);
13728
14282
  }
13729
14283
  }
@@ -13737,7 +14291,7 @@ async function agentContentEqual(tool, localPath, teamPath) {
13737
14291
  const canonicalContent = await readFileSafe(teamPath);
13738
14292
  const localContent = await readFileSafe(localPath);
13739
14293
  if (canonicalContent === null || localContent === null) return false;
13740
- const parsed = parseAgentYaml(canonicalContent, path37.basename(teamPath));
14294
+ const parsed = parseAgentYaml(canonicalContent, path38.basename(teamPath));
13741
14295
  if (!parsed.ok) return false;
13742
14296
  return localContent === renderForTool(parsed.spec, tool).content;
13743
14297
  }
@@ -13767,7 +14321,7 @@ function reverseByTool(tool, filePath, content) {
13767
14321
  return reverseFromOpencode(filePath, content);
13768
14322
  }
13769
14323
  }
13770
- var AgentsHandler;
14324
+ var AgentsHandler, LEGACY_MD_TOOLS;
13771
14325
  var init_agents = __esm({
13772
14326
  "src/resources/agents.ts"() {
13773
14327
  "use strict";
@@ -13776,6 +14330,9 @@ var init_agents = __esm({
13776
14330
  init_logger();
13777
14331
  init_types();
13778
14332
  init_builtin_agents();
14333
+ init_resource_namespaces();
14334
+ init_projects();
14335
+ init_path_safety();
13779
14336
  init_agent_format();
13780
14337
  AgentsHandler = class extends ResourceHandler {
13781
14338
  type = "agents";
@@ -13787,50 +14344,53 @@ var init_agents = __esm({
13787
14344
  * Built-in CLI agents are excluded from push.
13788
14345
  */
13789
14346
  async scanLocalForPush(teamConfig, localConfig) {
13790
- const teamAgentsDir = path37.join(localConfig.repo.localPath, "agents");
14347
+ const teamAgentsDir = path38.join(localConfig.repo.localPath, "agents");
13791
14348
  const tombstones = await this.readTombstones(localConfig);
13792
14349
  const baseDir = resolveBaseDir(localConfig);
13793
14350
  const directItems = [];
13794
14351
  const directStems = /* @__PURE__ */ new Set();
13795
14352
  if (isSelfMode(localConfig) && localConfig.projectRoot) {
13796
- const activeAgentsDir = path37.join(localConfig.projectRoot, ".teamai", "agents");
14353
+ const activeAgentsDir = path38.join(localConfig.projectRoot, ".teamai", "agents");
13797
14354
  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);
14355
+ for (const { dir, namespace } of await listTeamAgentDirs(activeAgentsDir)) {
14356
+ const relDir = namespace ? `agents/${namespace}` : "agents";
14357
+ for (const file of await listFiles(dir)) {
14358
+ const isYaml = file.endsWith(".yaml");
14359
+ const isMd = file.endsWith(".md");
14360
+ if (!isYaml && !isMd) continue;
14361
+ const stem = file.replace(/\.(yaml|md)$/, "");
14362
+ if (tombstones.has(stem)) continue;
14363
+ if (BUILTIN_AGENT_NAMES.has(stem)) continue;
14364
+ const activePath = path38.join(dir, file);
14365
+ const basePath = path38.join(localConfig.repo.localPath, relDir, file);
14366
+ const baseExists = await pathExists(basePath);
14367
+ if (baseExists && await fileContentEqual(activePath, basePath)) continue;
14368
+ directItems.push({
14369
+ name: stem,
14370
+ type: "agents",
14371
+ sourcePath: activePath,
14372
+ relativePath: `${relDir}/${file}`,
14373
+ status: baseExists ? "modified" : "new",
14374
+ legacy: isMd
14375
+ });
14376
+ directStems.add(stem);
14377
+ }
13818
14378
  }
13819
14379
  }
13820
14380
  }
13821
14381
  const grouped = /* @__PURE__ */ new Map();
13822
14382
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13823
14383
  if (!toolPath.agents) continue;
13824
- const agentsDir = path37.join(baseDir, toolPath.agents);
14384
+ const agentsDir = path38.join(baseDir, toolPath.agents);
13825
14385
  if (!await pathExists(agentsDir)) continue;
13826
14386
  const files = await listFiles(agentsDir);
13827
14387
  for (const file of files) {
13828
- const stem = getAgentStem(file);
14388
+ const stem = agentStemFromFilename(file);
13829
14389
  if (stem === null) continue;
13830
14390
  if (tombstones.has(stem)) continue;
13831
14391
  if (BUILTIN_AGENT_NAMES.has(stem)) continue;
13832
14392
  if (directStems.has(stem)) continue;
13833
- const filePath = path37.join(agentsDir, file);
14393
+ const filePath = path38.join(agentsDir, file);
13834
14394
  let toolGroup = grouped.get(stem);
13835
14395
  if (!toolGroup) {
13836
14396
  toolGroup = /* @__PURE__ */ new Map();
@@ -13842,11 +14402,41 @@ var init_agents = __esm({
13842
14402
  }
13843
14403
  }
13844
14404
  const items = [...directItems];
14405
+ const resolved = await resolveResourceNamespaces(localConfig);
14406
+ const activeNamespaces = resolved?.activeNamespaces.agents ?? null;
13845
14407
  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);
14408
+ const sources = await findTeamAgentFiles(teamAgentsDir, stem);
14409
+ const candidates = sources.filter(
14410
+ (file) => activeNamespaces === null || !file.namespace || activeNamespaces.includes(file.namespace)
14411
+ );
14412
+ if (candidates.length > 1) {
14413
+ items.push({
14414
+ name: stem,
14415
+ type: "agents",
14416
+ sourcePath: teamAgentsDir,
14417
+ relativePath: `agents/${stem}.yaml`,
14418
+ status: "modified",
14419
+ skipReason: `Ambiguous agent "${stem}": multiple active sources (${candidates.map((file) => file.path).join(", ")}). Give active agents unique names before pushing.`
14420
+ });
14421
+ continue;
14422
+ }
14423
+ if (sources.length > 0 && candidates.length === 0) {
14424
+ items.push({
14425
+ name: stem,
14426
+ type: "agents",
14427
+ sourcePath: teamAgentsDir,
14428
+ relativePath: `agents/${stem}.yaml`,
14429
+ status: "modified",
14430
+ skipReason: `Agent "${stem}" has no active source. Activate its role or project before pushing local edits.`
14431
+ });
14432
+ continue;
14433
+ }
14434
+ const located = candidates[0];
14435
+ const teamYamlPath = located?.ext === ".yaml" ? located.path : path38.join(teamAgentsDir, `${stem}.yaml`);
14436
+ const teamMdPath = located?.ext === ".md" ? located.path : path38.join(teamAgentsDir, `${stem}.md`);
14437
+ const hasTeamYaml = located?.ext === ".yaml";
14438
+ const hasTeamMd = located?.ext === ".md";
14439
+ const teamDir = located?.namespace ? `agents/${located.namespace}` : "agents";
13850
14440
  let canonicalSpec;
13851
14441
  if (hasTeamYaml) {
13852
14442
  const raw = await readFileSafe(teamYamlPath);
@@ -13856,7 +14446,7 @@ var init_agents = __esm({
13856
14446
  name: stem,
13857
14447
  type: "agents",
13858
14448
  sourcePath: teamYamlPath,
13859
- relativePath: `agents/${stem}.yaml`,
14449
+ relativePath: `${teamDir}/${stem}.yaml`,
13860
14450
  status: "modified",
13861
14451
  skipReason: "cannot read or parse canonical agent YAML"
13862
14452
  });
@@ -13934,7 +14524,7 @@ var init_agents = __esm({
13934
14524
  name: stem,
13935
14525
  type: "agents",
13936
14526
  sourcePath: bestPath,
13937
- relativePath: `agents/${stem}.yaml`,
14527
+ relativePath: `${teamDir}/${stem}.yaml`,
13938
14528
  status: status2,
13939
14529
  mergedSpec: mergeResult.spec
13940
14530
  });
@@ -13945,7 +14535,7 @@ var init_agents = __esm({
13945
14535
  name: stem,
13946
14536
  type: "agents",
13947
14537
  sourcePath: bestPath,
13948
- relativePath: `agents/${stem}.md`,
14538
+ relativePath: `${teamDir}/${stem}.md`,
13949
14539
  status: status2,
13950
14540
  skipReason
13951
14541
  });
@@ -13956,32 +14546,33 @@ var init_agents = __esm({
13956
14546
  * Scan team repo `agents/` for files to pull.
13957
14547
  * Recognizes both *.yaml (new) and *.md (legacy).
13958
14548
  * Hidden files (tombstones) are filtered out by listFiles.
14549
+ *
14550
+ * Root-level files are shared with everyone. One level of subdirectories
14551
+ * (`agents/<namespace>/`) carries role/project-scoped agents, the same
14552
+ * convention `rules/<namespace>/` uses; pull filters them by the active
14553
+ * `agents` namespaces. Deeper nesting is not scanned.
13959
14554
  */
13960
14555
  async scanTeamForPull(_teamConfig, localConfig) {
13961
- const agentsDir = path37.join(localConfig.repo.localPath, "agents");
14556
+ const agentsDir = path38.join(localConfig.repo.localPath, "agents");
13962
14557
  if (!await pathExists(agentsDir)) return [];
13963
- const files = await listFiles(agentsDir);
13964
14558
  const items = [];
13965
- for (const file of files) {
13966
- if (file.endsWith(".yaml")) {
13967
- const stem = file.replace(/\.yaml$/, "");
14559
+ const scanDir = async (dir, namespace) => {
14560
+ const prefix = namespace ? `agents/${namespace}` : "agents";
14561
+ for (const file of await listFiles(dir)) {
14562
+ const legacy = file.endsWith(".md");
14563
+ if (!legacy && !file.endsWith(".yaml")) continue;
13968
14564
  items.push({
13969
- name: stem,
13970
- 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,
14565
+ name: file.replace(/\.(yaml|md)$/, ""),
13979
14566
  type: "agents",
13980
- sourcePath: path37.join(agentsDir, file),
13981
- relativePath: `agents/${file}`,
13982
- legacy: true
14567
+ sourcePath: path38.join(dir, file),
14568
+ relativePath: `${prefix}/${file}`,
14569
+ legacy,
14570
+ ...namespace ? { namespace } : {}
13983
14571
  });
13984
14572
  }
14573
+ };
14574
+ for (const { dir, namespace } of await listTeamAgentDirs(agentsDir)) {
14575
+ await scanDir(dir, namespace);
13985
14576
  }
13986
14577
  return items;
13987
14578
  }
@@ -13994,22 +14585,27 @@ var init_agents = __esm({
13994
14585
  async pushItem(item, _teamConfig, localConfig) {
13995
14586
  const agentItem = item;
13996
14587
  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");
14588
+ log.warn(`[agents] Skipped ${item.name}: ${agentItem.skipReason}`);
13999
14589
  return;
14000
14590
  }
14591
+ const teamDir = path38.resolve(localConfig.repo.localPath, path38.dirname(item.relativePath));
14592
+ assertWithinRoot(
14593
+ path38.join(localConfig.repo.localPath, "agents"),
14594
+ teamDir,
14595
+ `Invalid agent destination outside team repo agents directory: ${item.relativePath}`
14596
+ );
14001
14597
  if (agentItem.mergedSpec) {
14002
- const dest2 = path37.join(localConfig.repo.localPath, "agents", `${item.name}.yaml`);
14003
- await ensureDir(path37.dirname(dest2));
14598
+ const dest2 = path38.join(teamDir, `${item.name}.yaml`);
14599
+ await ensureDir(path38.dirname(dest2));
14004
14600
  const yamlContent = serializeAgentYaml(agentItem.mergedSpec);
14005
14601
  await writeFile(dest2, yamlContent);
14006
14602
  log.debug(`Wrote agent ${item.name} \u2192 team repo (YAML format)`);
14007
14603
  return;
14008
14604
  }
14009
14605
  const ext = item.sourcePath.endsWith(".yaml") ? ".yaml" : ".md";
14010
- const dest = path37.join(localConfig.repo.localPath, "agents", `${item.name}${ext}`);
14606
+ const dest = path38.join(teamDir, `${item.name}${ext}`);
14011
14607
  if (item.sourcePath !== dest) {
14012
- await ensureDir(path37.dirname(dest));
14608
+ await ensureDir(path38.dirname(dest));
14013
14609
  await copyFile(item.sourcePath, dest);
14014
14610
  }
14015
14611
  log.debug(`Copied agent ${item.name} \u2192 team repo (${ext} verbatim)`);
@@ -14053,12 +14649,12 @@ var init_agents = __esm({
14053
14649
  continue;
14054
14650
  }
14055
14651
  if (isAgentExcluded(localConfig, tool)) continue;
14056
- const destDir = path37.join(baseDir, toolPath.agents);
14652
+ const destDir = path38.join(baseDir, toolPath.agents);
14057
14653
  try {
14058
14654
  await ensureDir(destDir);
14059
14655
  const { ext, content: rendered } = renderForTool(spec, tool);
14060
14656
  await removeStaleAgentSiblings2(destDir, item.name, ext);
14061
- const dest = path37.join(destDir, `${item.name}${ext}`);
14657
+ const dest = path38.join(destDir, `${item.name}${ext}`);
14062
14658
  await writeFile(dest, rendered);
14063
14659
  log.debug(`Rendered agent ${item.name} \u2192 ${tool} (${ext})`);
14064
14660
  } catch (e) {
@@ -14074,19 +14670,17 @@ var init_agents = __esm({
14074
14670
  async removeItem(name, teamConfig, localConfig) {
14075
14671
  const removed = [];
14076
14672
  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
- }
14673
+ const teamAgentsDir = path38.join(localConfig.repo.localPath, "agents");
14674
+ for (const located of await findTeamAgentFiles(teamAgentsDir, name)) {
14675
+ await remove(located.path);
14676
+ removed.push(located.path);
14084
14677
  }
14085
14678
  await this.addTombstone(name, localConfig);
14086
14679
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14087
14680
  if (!toolPath.agents) continue;
14088
- for (const ext of [".md", ".toml", ".json"]) {
14089
- const filePath = path37.join(baseDir, toolPath.agents, `${name}${ext}`);
14681
+ if (isAgentExcluded(localConfig, tool)) continue;
14682
+ for (const ext of AGENT_FILE_EXTENSIONS) {
14683
+ const filePath = path38.join(baseDir, toolPath.agents, `${name}${ext}`);
14090
14684
  if (await pathExists(filePath)) {
14091
14685
  await remove(filePath);
14092
14686
  removed.push(filePath);
@@ -14096,14 +14690,69 @@ var init_agents = __esm({
14096
14690
  }
14097
14691
  return removed;
14098
14692
  }
14693
+ /**
14694
+ * Revocation pass for role/project scoping. Removes the deployed copies of
14695
+ * every agent whose namespace is no longer active, on every installed tool.
14696
+ *
14697
+ * Data-safety gate, same as inactive skills: a file is deleted only when it
14698
+ * is byte-equal to what pull would render from the team source. A local edit
14699
+ * is kept and reported so nothing unpushed is lost. Root-level agents and
14700
+ * agents still deployed to the same tool destination never qualify.
14701
+ */
14702
+ async cleanupInactiveNamespaces(teamConfig, localConfig, activeNamespaces) {
14703
+ const items = await this.scanTeamForPull(teamConfig, localConfig);
14704
+ const isActive = (item) => !item.namespace || activeNamespaces.includes(item.namespace);
14705
+ const active = items.filter(isActive);
14706
+ const inactive = items.filter((item) => !isActive(item) && !BUILTIN_AGENT_NAMES.has(item.name));
14707
+ if (inactive.length === 0) return;
14708
+ const baseDir = resolveBaseDir(localConfig);
14709
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14710
+ if (!toolPath.agents || !isKnownTool(tool) || isAgentExcluded(localConfig, tool)) continue;
14711
+ if (!await ResourceHandler.isToolInstalled(toolPath.agents, baseDir)) continue;
14712
+ const destDir = path38.join(baseDir, toolPath.agents);
14713
+ const activeDestinations = /* @__PURE__ */ new Set();
14714
+ for (const item of active) {
14715
+ const rendered = await this.renderedForTool(item, tool);
14716
+ if (rendered) activeDestinations.add(`${item.name}${rendered.ext}`);
14717
+ }
14718
+ for (const item of inactive) {
14719
+ const expected = await this.renderedForTool(item, tool);
14720
+ if (!expected || activeDestinations.has(`${item.name}${expected.ext}`)) continue;
14721
+ const deployed = path38.join(destDir, `${item.name}${expected.ext}`);
14722
+ const current = await readFileSafe(deployed);
14723
+ if (current === null) continue;
14724
+ if (current !== expected.content) {
14725
+ log.warn(`[${localConfig.scope}] Kept agent "${item.name}" (${tool}): it differs from the team source ${item.relativePath}. Back it up, then delete it manually.`);
14726
+ continue;
14727
+ }
14728
+ await remove(deployed);
14729
+ log.debug(`[${localConfig.scope}] Removed inactive role-scoped agent ${item.name} from ${tool}`);
14730
+ }
14731
+ }
14732
+ }
14733
+ /**
14734
+ * What `pullItem` writes for this agent on this tool, or null when the tool
14735
+ * is not a target (legacy `.md` only reaches LEGACY_MD_TOOLS, a YAML spec
14736
+ * honours `targets`, an unparsable spec is skipped like pull skips it).
14737
+ */
14738
+ async renderedForTool(item, tool) {
14739
+ const content = await readFileSafe(item.sourcePath);
14740
+ if (content === null) return null;
14741
+ if (item.legacy) {
14742
+ return LEGACY_MD_TOOLS.has(tool) ? { ext: ".md", content } : null;
14743
+ }
14744
+ const parsed = parseAgentYaml(content, `${item.name}.yaml`);
14745
+ if (!parsed.ok) return null;
14746
+ if (parsed.spec.targets && !parsed.spec.targets.includes(tool)) return null;
14747
+ return renderForTool(parsed.spec, tool);
14748
+ }
14099
14749
  // ─── Private helpers ──────────────────────────────────────────────────────
14100
14750
  /**
14101
14751
  * Legacy pull: copies .md as-is to Claude-compatible tools, including JoyCode.
14102
14752
  */
14103
14753
  async pullLegacyMd(item, teamConfig, baseDir, localConfig) {
14104
- const legacyTools = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
14105
14754
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14106
- if (!legacyTools.has(tool)) continue;
14755
+ if (!LEGACY_MD_TOOLS.has(tool)) continue;
14107
14756
  if (!toolPath.agents) {
14108
14757
  log.debug(`Skipping legacy agent sync for ${tool}: no agents path configured`);
14109
14758
  continue;
@@ -14113,10 +14762,10 @@ var init_agents = __esm({
14113
14762
  continue;
14114
14763
  }
14115
14764
  if (isAgentExcluded(localConfig, tool)) continue;
14116
- const destDir = path37.join(baseDir, toolPath.agents);
14765
+ const destDir = path38.join(baseDir, toolPath.agents);
14117
14766
  try {
14118
14767
  await ensureDir(destDir);
14119
- const dest = path37.join(destDir, `${item.name}.md`);
14768
+ const dest = path38.join(destDir, `${item.name}.md`);
14120
14769
  await copyFile(item.sourcePath, dest);
14121
14770
  log.debug(`Synced legacy agent ${item.name} \u2192 ${tool}`);
14122
14771
  } catch (e) {
@@ -14125,6 +14774,7 @@ var init_agents = __esm({
14125
14774
  }
14126
14775
  }
14127
14776
  };
14777
+ LEGACY_MD_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
14128
14778
  }
14129
14779
  });
14130
14780
 
@@ -14190,11 +14840,11 @@ __export(update_exports, {
14190
14840
  resolveRegistryForPackage: () => resolveRegistryForPackage,
14191
14841
  update: () => update
14192
14842
  });
14193
- import { execFile as execFile3 } from "child_process";
14843
+ import { execFile as execFile2 } from "child_process";
14194
14844
  import { promisify as promisify2 } from "util";
14195
14845
  import { randomUUID as randomUUID2 } from "crypto";
14196
14846
  import fs16 from "fs";
14197
- import path38 from "path";
14847
+ import path39 from "path";
14198
14848
  import { fileURLToPath as fileURLToPath4 } from "url";
14199
14849
  import fse7 from "fs-extra";
14200
14850
  function resolveRegistryForPackage(pkgName) {
@@ -14204,15 +14854,15 @@ function resolveRegistryForPackage(pkgName) {
14204
14854
  return PUBLIC_REGISTRY;
14205
14855
  }
14206
14856
  function resolveNpmCommand() {
14207
- const nodeDir = path38.dirname(process.execPath);
14857
+ const nodeDir = path39.dirname(process.execPath);
14208
14858
  const candidates = [
14209
14859
  // Bundled runtimes: npm installed flat next to node.exe.
14210
- path38.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
14860
+ path39.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
14211
14861
  // POSIX layout rooted at the node dir itself.
14212
- path38.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
14862
+ path39.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
14213
14863
  // Canonical POSIX install (official tarball, Homebrew, nvm): node lives in
14214
14864
  // <prefix>/bin with npm at <prefix>/lib/node_modules — one level up.
14215
- path38.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
14865
+ path39.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
14216
14866
  ];
14217
14867
  for (const c of candidates) {
14218
14868
  if (fs16.existsSync(c)) return { cmd: process.execPath, args: [c] };
@@ -14221,16 +14871,16 @@ function resolveNpmCommand() {
14221
14871
  return { cmd: "npm", args: [] };
14222
14872
  }
14223
14873
  function prefixFromEntryPath(entry, posix) {
14224
- const marker = `${path38.sep}node_modules${path38.sep}`;
14874
+ const marker = `${path39.sep}node_modules${path39.sep}`;
14225
14875
  const idx = entry.lastIndexOf(marker);
14226
14876
  if (idx <= 0) return null;
14227
14877
  const root = entry.slice(0, idx);
14228
14878
  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;
14879
+ if (posix && path39.basename(root) === "lib") {
14880
+ const prefix = path39.dirname(root);
14881
+ return fs16.existsSync(path39.join(prefix, "lib", "node_modules", pkgDir)) ? { prefix, global: true } : null;
14232
14882
  }
14233
- return fs16.existsSync(path38.join(root, "node_modules", pkgDir)) ? { prefix: root, global: !posix } : null;
14883
+ return fs16.existsSync(path39.join(root, "node_modules", pkgDir)) ? { prefix: root, global: !posix } : null;
14234
14884
  }
14235
14885
  function resolveInstallPrefix() {
14236
14886
  return prefixFromEntryPath(fileURLToPath4(import.meta.url), process.platform !== "win32");
@@ -14355,7 +15005,7 @@ async function acquireLock(lockPath) {
14355
15005
  owner
14356
15006
  });
14357
15007
  try {
14358
- await ensureDir(path38.dirname(resolved));
15008
+ await ensureDir(path39.dirname(resolved));
14359
15009
  } catch {
14360
15010
  return false;
14361
15011
  }
@@ -14484,12 +15134,12 @@ async function doUpdate() {
14484
15134
  if (entry) {
14485
15135
  try {
14486
15136
  const installed = JSON.parse(fs16.readFileSync(
14487
- path38.join(path38.dirname(path38.dirname(entry)), "package.json"),
15137
+ path39.join(path39.dirname(path39.dirname(entry)), "package.json"),
14488
15138
  "utf-8"
14489
15139
  ));
14490
15140
  if (installed.version !== result.latest) {
14491
15141
  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.`
15142
+ `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
15143
  );
14494
15144
  }
14495
15145
  } catch {
@@ -14543,7 +15193,7 @@ var init_update = __esm({
14543
15193
  init_types();
14544
15194
  init_prompt();
14545
15195
  init_package_info();
14546
- execFileAsync2 = promisify2(execFile3);
15196
+ execFileAsync2 = promisify2(execFile2);
14547
15197
  PUBLIC_REGISTRY = "https://registry.npmjs.org";
14548
15198
  TNPM_REGISTRY = "http://r.tnpm.oa.com";
14549
15199
  VERSION_CHECK_TIMEOUT = 5e3;
@@ -14558,9 +15208,9 @@ var marketplace_exports = {};
14558
15208
  __export(marketplace_exports, {
14559
15209
  refreshMarketplace: () => refreshMarketplace
14560
15210
  });
14561
- import path39 from "path";
15211
+ import path40 from "path";
14562
15212
  async function extractSkillDescription2(skillDir) {
14563
- const skillMdPath = path39.join(skillDir, SKILL_MD2);
15213
+ const skillMdPath = path40.join(skillDir, SKILL_MD2);
14564
15214
  const content = await readFileSafe(skillMdPath);
14565
15215
  if (!content) return "";
14566
15216
  const { data } = parseFrontmatter(content);
@@ -14569,7 +15219,7 @@ async function extractSkillDescription2(skillDir) {
14569
15219
  return String(desc).replace(/\s+/g, " ").trim();
14570
15220
  }
14571
15221
  async function refreshMarketplace(repoPath) {
14572
- const marketplacePath = path39.join(repoPath, MARKETPLACE_PATH);
15222
+ const marketplacePath = path40.join(repoPath, MARKETPLACE_PATH);
14573
15223
  if (!await pathExists(marketplacePath)) {
14574
15224
  return false;
14575
15225
  }
@@ -14583,11 +15233,11 @@ async function refreshMarketplace(repoPath) {
14583
15233
  return false;
14584
15234
  }
14585
15235
  if (!Array.isArray(marketplace.plugins)) return false;
14586
- const skillsDir = path39.join(repoPath, "skills");
15236
+ const skillsDir = path40.join(repoPath, "skills");
14587
15237
  const currentSkills = /* @__PURE__ */ new Set();
14588
15238
  const dirs = await listDirs(skillsDir);
14589
15239
  for (const dir of dirs) {
14590
- const hasSkillMd = await pathExists(path39.join(skillsDir, dir, SKILL_MD2));
15240
+ const hasSkillMd = await pathExists(path40.join(skillsDir, dir, SKILL_MD2));
14591
15241
  if (hasSkillMd) {
14592
15242
  currentSkills.add(dir);
14593
15243
  }
@@ -14606,7 +15256,7 @@ async function refreshMarketplace(repoPath) {
14606
15256
  for (const skillName of currentSkills) {
14607
15257
  const existing = existingPlugins.get(skillName);
14608
15258
  if (existing) {
14609
- const desc = await extractSkillDescription2(path39.join(skillsDir, skillName));
15259
+ const desc = await extractSkillDescription2(path40.join(skillsDir, skillName));
14610
15260
  if (desc && desc !== existing.description) {
14611
15261
  existing.description = desc;
14612
15262
  changed = true;
@@ -14614,7 +15264,7 @@ async function refreshMarketplace(repoPath) {
14614
15264
  updatedPlugins.push(existing);
14615
15265
  existingPlugins.delete(skillName);
14616
15266
  } else {
14617
- const desc = await extractSkillDescription2(path39.join(skillsDir, skillName));
15267
+ const desc = await extractSkillDescription2(path40.join(skillsDir, skillName));
14618
15268
  updatedPlugins.push({
14619
15269
  name: skillName,
14620
15270
  source: `./skills/${skillName}`,
@@ -14651,177 +15301,6 @@ var init_marketplace = __esm({
14651
15301
  }
14652
15302
  });
14653
15303
 
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
15304
  // src/pkg/types.ts
14826
15305
  import { z as z7 } from "zod";
14827
15306
  var PackageScopeSchema, NpmSpecSchema, ClaudeMarketplaceSchema, ClaudePluginSpecSchema, ClaudeEcosystemSchema, PackageSetSchema, PackageManifestSchema, NpmLockEntrySchema, ClaudeMarketplaceLockSchema, ClaudePluginLockSchema, PackageLockSchema;
@@ -15073,6 +15552,7 @@ __export(reports_branch_exports, {
15073
15552
  ensureReportsDir: () => ensureReportsDir,
15074
15553
  ensureReportsWorktree: () => ensureReportsWorktree,
15075
15554
  refreshReportsWorktree: () => refreshReportsWorktree,
15555
+ updateReports: () => updateReports,
15076
15556
  withKnowledgeWorktree: () => withKnowledgeWorktree
15077
15557
  });
15078
15558
  import path42 from "path";
@@ -15196,6 +15676,38 @@ async function writeWorktreeGitignore(wt) {
15196
15676
  ].join("\n");
15197
15677
  await writeFile(path42.join(wt, ".gitignore"), content);
15198
15678
  }
15679
+ async function commitAndPushReportsAt(wt, message, files, options = {}) {
15680
+ const git = createGit2(wt);
15681
+ await git.add(files);
15682
+ const status2 = await git.status();
15683
+ if (status2.staged.length === 0 && !options.pushIfUnchanged) {
15684
+ log.debug("[reports] nothing to commit");
15685
+ return false;
15686
+ }
15687
+ if (status2.staged.length > 0) await commitSkippingHooks(git, message);
15688
+ for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
15689
+ try {
15690
+ await git.push(["origin", REPORTS_BRANCH]);
15691
+ return true;
15692
+ } catch (pushErr) {
15693
+ if (attempt === MAX_PUSH_RETRIES) {
15694
+ log.debug(`[reports] push failed after ${attempt} attempts: ${pushErr.message}`);
15695
+ return false;
15696
+ }
15697
+ try {
15698
+ await git.fetch(["origin", REPORTS_BRANCH]);
15699
+ await git.rebase([`origin/${REPORTS_BRANCH}`]);
15700
+ } catch (rebaseErr) {
15701
+ log.debug(`[reports] rebase failed, retrying: ${rebaseErr.message}`);
15702
+ try {
15703
+ await git.rebase(["--abort"]);
15704
+ } catch {
15705
+ }
15706
+ }
15707
+ }
15708
+ }
15709
+ return false;
15710
+ }
15199
15711
  async function commitAndPushReports(localConfig, message, files, options = {}) {
15200
15712
  const lockPath = reportsLockPath(localConfig);
15201
15713
  const locked = await acquireLock(lockPath);
@@ -15205,38 +15717,37 @@ async function commitAndPushReports(localConfig, message, files, options = {}) {
15205
15717
  }
15206
15718
  try {
15207
15719
  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;
15720
+ return await commitAndPushReportsAt(wt, message, files, options);
15721
+ } catch (e) {
15722
+ log.debug(`[reports] commitAndPushReports failed (non-blocking): ${e.message}`);
15723
+ return false;
15724
+ } finally {
15725
+ await releaseLock(lockPath);
15726
+ }
15727
+ }
15728
+ async function updateReports(localConfig, write, options = {}) {
15729
+ if (!usesReportsBranch(localConfig)) {
15730
+ throw new Error("updateReports requires a repo that stores reports on the teamai-reports branch");
15731
+ }
15732
+ const lockPath = reportsLockPath(localConfig);
15733
+ if (!await acquireLock(lockPath)) {
15734
+ log.debug("[reports] another reports write is in progress; skipping");
15735
+ return false;
15736
+ }
15737
+ try {
15738
+ const wt = await ensureReportsWorktree(localConfig);
15739
+ try {
15740
+ await syncReportsWorktree(wt);
15741
+ } catch (e) {
15742
+ log.debug(`[reports] sync before write failed, writing onto the local copy: ${e.message}`);
15214
15743
  }
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
- }
15744
+ const change = await write(wt);
15745
+ if (!change || change.files.length === 0) {
15746
+ return false;
15236
15747
  }
15237
- return false;
15748
+ return await commitAndPushReportsAt(wt, change.message, change.files, options);
15238
15749
  } catch (e) {
15239
- log.debug(`[reports] commitAndPushReports failed (non-blocking): ${e.message}`);
15750
+ log.debug(`[reports] updateReports failed (non-blocking): ${e.message}`);
15240
15751
  return false;
15241
15752
  } finally {
15242
15753
  await releaseLock(lockPath);
@@ -15593,6 +16104,10 @@ async function detectHomeInstalledAgents(candidateIds = SELF_MODE_AGENT_CHOICES)
15593
16104
  const home = getUserHome();
15594
16105
  const found = [];
15595
16106
  for (const id of candidateIds) {
16107
+ if (id === COPILOT_TOOL_ID) {
16108
+ if (await pathExists(getCopilotHome())) found.push(id);
16109
+ continue;
16110
+ }
15596
16111
  const skillsPath = KNOWN_AGENTS.find((a) => a.id === id)?.skillsPath;
15597
16112
  if (!skillsPath) continue;
15598
16113
  const rootSegment = skillsPath.split("/")[0];
@@ -15627,7 +16142,6 @@ function getEffectiveAgents(teamConfig, localConfig) {
15627
16142
  return [...byId.values()];
15628
16143
  }
15629
16144
  async function detectInstalledAgents(localConfig, teamConfig) {
15630
- const baseDir = resolveBaseDir(localConfig);
15631
16145
  const agents = getEffectiveAgents(teamConfig, localConfig);
15632
16146
  const scoped = scopedToolPaths(teamConfig, localConfig);
15633
16147
  const fromTeamConfig = new Set(
@@ -15635,12 +16149,11 @@ async function detectInstalledAgents(localConfig, teamConfig) {
15635
16149
  );
15636
16150
  const results = [];
15637
16151
  for (const agent of agents) {
15638
- const rootSegment = toolInstallRoot(agent.skillsPath);
15639
- const rootPath = path44.join(baseDir, rootSegment);
15640
- const installed = rootSegment ? await pathExists(rootPath) : false;
16152
+ const baseDir = resolveToolBaseDir(agent.id, localConfig);
16153
+ const installed = await isToolInstalledForConfig(agent.id, agent.skillsPath, localConfig);
15641
16154
  results.push({
15642
16155
  ...agent,
15643
- absoluteSkillsPath: `${baseDir}/${agent.skillsPath}`,
16156
+ absoluteSkillsPath: path44.join(baseDir, agent.skillsPath),
15644
16157
  installed,
15645
16158
  fromTeamConfig: fromTeamConfig.has(agent.id)
15646
16159
  });
@@ -15655,7 +16168,7 @@ var init_known_agents = __esm({
15655
16168
  init_types();
15656
16169
  init_base();
15657
16170
  init_home();
15658
- SELF_MODE_AGENT_CHOICES = ["claude", "codex", "cursor", "joycode", "codebuddy", "workbuddy"];
16171
+ SELF_MODE_AGENT_CHOICES = ["claude", "codex", "cursor", "copilot", "joycode", "codebuddy", "workbuddy"];
15659
16172
  KNOWN_AGENTS = [
15660
16173
  // Coding agents already wired through teamConfig.toolPaths defaults
15661
16174
  { id: "claude", displayName: "Claude Code", category: "coding", skillsPath: ".claude/skills" },
@@ -15706,6 +16219,7 @@ var init_known_agents = __esm({
15706
16219
  // src/init.ts
15707
16220
  var init_exports = {};
15708
16221
  __export(init_exports, {
16222
+ ALL_PROJECTS_SELECTOR: () => ALL_PROJECTS_SELECTOR,
15709
16223
  buildSelfModeGitignore: () => buildSelfModeGitignore,
15710
16224
  init: () => init,
15711
16225
  initHttp: () => initHttp,
@@ -15713,6 +16227,7 @@ __export(init_exports, {
15713
16227
  migrateSelfModeGitignore: () => migrateSelfModeGitignore,
15714
16228
  migrateSelfModeGitignoreContent: () => migrateSelfModeGitignoreContent,
15715
16229
  promptForSelfModeAgents: () => promptForSelfModeAgents,
16230
+ resolveActiveProjects: () => resolveActiveProjects,
15716
16231
  resolveInheritUserScope: () => resolveInheritUserScope,
15717
16232
  resolveInitRepo: () => resolveInitRepo,
15718
16233
  resolveInitScope: () => resolveInitScope,
@@ -15783,6 +16298,10 @@ async function promptForRoleProfile(repoPath, roleFlag) {
15783
16298
  resourceProfileVersion: manifest.version
15784
16299
  };
15785
16300
  }
16301
+ function dedupeIds(ids) {
16302
+ const seen = /* @__PURE__ */ new Set();
16303
+ return ids.filter((id) => seen.has(id) ? false : (seen.add(id), true));
16304
+ }
15786
16305
  async function resolveActiveProjects(repoPath, projectFlag) {
15787
16306
  const requested = (projectFlag ?? "").split(",").map((s) => s.trim()).filter(Boolean);
15788
16307
  if (requested.length === 0) {
@@ -15794,7 +16313,27 @@ async function resolveActiveProjects(repoPath, projectFlag) {
15794
16313
  `--project given but no projects manifest (manifest/projects.yaml) exists in the team repo.`
15795
16314
  );
15796
16315
  }
15797
- const validIds = new Set(listProjectIds(manifest));
16316
+ const declared = listProjectIds(manifest);
16317
+ if (requested.includes(ALL_PROJECTS_SELECTOR)) {
16318
+ if (requested.length > 1) {
16319
+ throw new Error(
16320
+ `--project "${ALL_PROJECTS_SELECTOR}" already covers every declared project; drop the other ids (got: ${requested.join(", ")}).`
16321
+ );
16322
+ }
16323
+ if (declared.length === 0) {
16324
+ log.warn(
16325
+ `--project "${ALL_PROJECTS_SELECTOR}" was given but manifest/projects.yaml declares no projects; nothing was activated.`
16326
+ );
16327
+ return { projects: [] };
16328
+ }
16329
+ if (declared.includes(ALL_PROJECTS_SELECTOR)) {
16330
+ log.warn(
16331
+ `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}\`.`
16332
+ );
16333
+ }
16334
+ return { projects: declared };
16335
+ }
16336
+ const validIds = new Set(declared);
15798
16337
  for (const id of requested) {
15799
16338
  if (!validIds.has(id)) {
15800
16339
  throw new Error(
@@ -15802,9 +16341,7 @@ async function resolveActiveProjects(repoPath, projectFlag) {
15802
16341
  );
15803
16342
  }
15804
16343
  }
15805
- const seen = /* @__PURE__ */ new Set();
15806
- const projects = requested.filter((id) => seen.has(id) ? false : (seen.add(id), true));
15807
- return { projects };
16344
+ return { projects: dedupeIds(requested) };
15808
16345
  }
15809
16346
  function resolveInitScope(rawScope, cwd, homeDir) {
15810
16347
  const cwdResolved = resolveRealPath(cwd);
@@ -16322,20 +16859,28 @@ async function initSelfRepo(options) {
16322
16859
  }
16323
16860
  if (!options.dryRun) {
16324
16861
  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
16862
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16863
+ let isNewSelfMember = false;
16864
+ let selfMemberChanged = false;
16865
+ const pushed = await updateReports2(localConfig, async (wt) => {
16866
+ const memberDir = path45.join(wt, "members");
16867
+ await ensureDir(memberDir);
16868
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
16869
+ isNewSelfMember = !await pathExists(memberPath);
16870
+ const existingSelfMember = await getMemberConfig(wt, username);
16871
+ const merged = mergeMemberConfig(existingSelfMember, {
16872
+ username,
16873
+ projects: localConfig.projects
16874
+ });
16875
+ selfMemberChanged = merged.changed;
16876
+ if (!merged.changed) return null;
16877
+ await writeFile(memberPath, YAML11.stringify(merged.config));
16878
+ return {
16879
+ files: ["members/"],
16880
+ message: isNewSelfMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`
16881
+ };
16335
16882
  });
16336
16883
  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
16884
  if (pushed) {
16340
16885
  log.success(isNewSelfMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
16341
16886
  } else {
@@ -16510,6 +17055,17 @@ async function init(options) {
16510
17055
  }
16511
17056
  } else {
16512
17057
  log.info(`Repo already exists at ${localPath}, using existing clone`);
17058
+ try {
17059
+ const pullResult = await pullRepoFastForward(localPath);
17060
+ if (pullResult !== "already up to date") {
17061
+ log.info(`Refreshed existing clone (${pullResult})`);
17062
+ }
17063
+ } catch (e) {
17064
+ log.error(
17065
+ `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.`
17066
+ );
17067
+ process.exit(1);
17068
+ }
16513
17069
  }
16514
17070
  } else {
16515
17071
  log.warn(`Existing ${localPath} is not a git repository, re-cloning`);
@@ -16656,29 +17212,37 @@ async function init(options) {
16656
17212
  let isNewMember = true;
16657
17213
  if (!options.dryRun) {
16658
17214
  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
17215
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
17216
+ let memberChanged = false;
17217
+ let memberProjects;
17218
+ const pushed = await updateReports2(reportsConfig, async (wt) => {
17219
+ const memberDir = path45.join(wt, "members");
17220
+ await ensureDir(memberDir);
17221
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
17222
+ isNewMember = !await pathExists(memberPath);
17223
+ const existingMember = await getMemberConfig(wt, username);
17224
+ const merged = mergeMemberConfig(existingMember, {
17225
+ username,
17226
+ projects: resolvedProjects
17227
+ });
17228
+ memberChanged = merged.changed;
17229
+ memberProjects = merged.config.projects;
17230
+ if (!merged.changed) return null;
17231
+ await writeFile(memberPath, YAML11.stringify(merged.config));
17232
+ return {
17233
+ files: ["members/"],
17234
+ message: isNewMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`
17235
+ };
16669
17236
  });
16670
17237
  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/"]);
17238
+ log.success(isNewMember ? `Registered as team member: ${username}` : `Updated member roster: ${username}${memberProjects ? ` (projects: ${memberProjects.join(", ")})` : ""}`);
16674
17239
  if (pushed) {
16675
17240
  log.success(isNewMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
16676
17241
  } else {
16677
17242
  log.warn("Member registration could not be pushed (no write access?). You are still set up locally.");
16678
17243
  }
16679
- } else {
17244
+ } else if (!isNewMember) {
16680
17245
  log.info(`Member ${username} already registered`);
16681
- isNewMember = false;
16682
17246
  }
16683
17247
  } catch (e) {
16684
17248
  log.warn(`Member registration skipped (non-blocking): ${e.message}`);
@@ -16804,6 +17368,7 @@ async function init(options) {
16804
17368
  log.info("Run `teamai status` to check current config.");
16805
17369
  closePrompt();
16806
17370
  }
17371
+ var ALL_PROJECTS_SELECTOR;
16807
17372
  var init_init = __esm({
16808
17373
  "src/init.ts"() {
16809
17374
  "use strict";
@@ -16822,6 +17387,7 @@ var init_init = __esm({
16822
17387
  init_members();
16823
17388
  init_prompt();
16824
17389
  init_known_agents();
17390
+ ALL_PROJECTS_SELECTOR = "all";
16825
17391
  }
16826
17392
  });
16827
17393
 
@@ -17144,13 +17710,19 @@ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConf
17144
17710
  }
17145
17711
  } : teamConfig;
17146
17712
  const pushableTypes = ["skills", "rules", "env", "agents"];
17147
- const allItems = [];
17713
+ const fullScan = [];
17148
17714
  for (const type of pushableTypes) {
17149
17715
  const handler = getHandler(type);
17150
17716
  const items = await handler.scanLocalForPush(scanTeamConfig, localConfig);
17151
- allItems.push(...items);
17717
+ fullScan.push(...items);
17152
17718
  }
17153
- const fullScan = [...allItems];
17719
+ const allItems = fullScan.filter((item) => {
17720
+ if (item.type === "agents" && "skipReason" in item && typeof item.skipReason === "string" && item.skipReason) {
17721
+ log.warn(`[agents] Skipped ${item.name}: ${item.skipReason}`);
17722
+ return false;
17723
+ }
17724
+ return true;
17725
+ });
17154
17726
  spin.stop();
17155
17727
  if (options.skill) {
17156
17728
  const skillBasename = path46.basename(
@@ -17536,6 +18108,7 @@ __export(git_exports, {
17536
18108
  listWorktrees: () => listWorktrees,
17537
18109
  normalizeRepoUrlForCompare: () => normalizeRepoUrlForCompare,
17538
18110
  pullRepo: () => pullRepo,
18111
+ pullRepoFastForward: () => pullRepoFastForward,
17539
18112
  pushLearningToOrigin: () => pushLearningToOrigin,
17540
18113
  pushRepoBranch: () => pushRepoBranch,
17541
18114
  pushRepoDirectly: () => pushRepoDirectly,
@@ -17674,6 +18247,14 @@ async function pullRepo(localPath) {
17674
18247
  return "reset to origin (diverged)";
17675
18248
  }
17676
18249
  }
18250
+ async function pullRepoFastForward(localPath) {
18251
+ const git = createGit2(localPath);
18252
+ const result = await git.pull(["--ff-only"]);
18253
+ if (result.summary.changes === 0 && result.summary.insertions === 0 && result.summary.deletions === 0) {
18254
+ return "already up to date";
18255
+ }
18256
+ return `${result.summary.changes} file(s) changed`;
18257
+ }
17677
18258
  async function getDefaultBranch(localPath) {
17678
18259
  const cached = defaultBranchCache.get(localPath);
17679
18260
  if (cached) return cached;
@@ -18249,19 +18830,19 @@ async function bootstrapSelfRepo(dir, opts) {
18249
18830
  log.debug(`[bootstrap] hook injection failed (non-blocking): ${e.message}`);
18250
18831
  }
18251
18832
  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)) {
18833
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
18834
+ await updateReports2(localConfig, async (wt) => {
18835
+ const memberDir = path49.join(wt, "members");
18836
+ await ensureDir(memberDir);
18837
+ const memberPath = path49.join(memberDir, `${username}.yaml`);
18838
+ if (await pathExists(memberPath)) return null;
18258
18839
  await writeFile(memberPath, YAML14.stringify({
18259
18840
  username,
18260
18841
  displayName: username,
18261
18842
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
18262
18843
  }));
18263
- await commitAndPushReports2(localConfig, `[teamai] Register member: ${username}`, ["members/"]);
18264
- }
18844
+ return { files: ["members/"], message: `[teamai] Register member: ${username}` };
18845
+ });
18265
18846
  } catch (e) {
18266
18847
  log.debug(`[bootstrap] member registration skipped (non-blocking): ${e.message}`);
18267
18848
  }
@@ -18529,68 +19110,6 @@ var init_config = __esm({
18529
19110
  }
18530
19111
  });
18531
19112
 
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
19113
  // src/pkg/adapters/base.ts
18595
19114
  var PackageAdapter;
18596
19115
  var init_base2 = __esm({
@@ -19596,9 +20115,9 @@ async function verifyStaging(legacyDir, staging) {
19596
20115
  throw new Error("migration verify: team-repo/.git missing after copy (clone would be broken)");
19597
20116
  }
19598
20117
  try {
19599
- const { execFile: execFile5 } = await import("child_process");
20118
+ const { execFile: execFile4 } = await import("child_process");
19600
20119
  const { promisify: promisify4 } = await import("util");
19601
- await promisify4(execFile5)("git", ["rev-parse", "HEAD"], { cwd: stagedRepo });
20120
+ await promisify4(execFile4)("git", ["rev-parse", "HEAD"], { cwd: stagedRepo });
19602
20121
  } catch (e) {
19603
20122
  throw new Error(
19604
20123
  `migration verify: the staged team-repo clone is not a usable git repository (${e.message})`
@@ -19983,6 +20502,7 @@ var init_tokenizer = __esm({
19983
20502
  // src/votes.ts
19984
20503
  var votes_exports = {};
19985
20504
  __export(votes_exports, {
20505
+ hasPendingVoteDeltas: () => hasPendingVoteDeltas,
19986
20506
  incrementRecalled: () => incrementRecalled,
19987
20507
  incrementUpvoted: () => incrementUpvoted,
19988
20508
  loadUserVotes: () => loadUserVotes,
@@ -20097,6 +20617,10 @@ function mergeDeltas(local, remote) {
20097
20617
  }
20098
20618
  return { version: 2, votes, deltas: {} };
20099
20619
  }
20620
+ async function hasPendingVoteDeltas(localVotesDir, username) {
20621
+ const local = await loadUserVotes(path57.join(localVotesDir, `${username}.yaml`));
20622
+ return Object.keys(local.deltas).length > 0;
20623
+ }
20100
20624
  async function syncVotesToTeam(repoPath, username, localVotesDir) {
20101
20625
  const localVotePath = path57.join(localVotesDir, `${username}.yaml`);
20102
20626
  const remoteVotePath = path57.join(repoPath, "votes", `${username}.yaml`);
@@ -22537,10 +23061,67 @@ async function reportUsageToTeam(repoPath, username, options) {
22537
23061
  const hasInterventions = hasInterventionDelta(interventionDelta);
22538
23062
  const hasPromptTokens = hasPromptTokenDelta(promptTokenDelta);
22539
23063
  const hasDaily = hasDailyDelta(dailyDelta);
22540
- let writeRoot = repoPath;
23064
+ const hasStats = hasUsage || hasInterventions || hasPromptTokens || hasDaily;
23065
+ const commitMsg = hasUsage ? `[teamai] Update usage stats for ${username}` : hasInterventions || hasPromptTokens || hasDaily ? `[teamai] Update session stats for ${username}` : `[teamai] Update votes for ${username}`;
23066
+ const writeReportFiles = async (writeRoot) => {
23067
+ if (hasStats) {
23068
+ const statsDir = path64.join(writeRoot, "stats");
23069
+ await ensureDir(statsDir);
23070
+ const statsPath = path64.join(statsDir, `${username}.yaml`);
23071
+ const existing = await readExistingStats(statsPath);
23072
+ if (useReportsBranch) {
23073
+ const previousContent = await readFileSafe(statsPath);
23074
+ restoreStats = () => writeFile(statsPath, previousContent ?? "");
23075
+ }
23076
+ const newStats = hasUsage ? aggregateUsage(events) : [];
23077
+ const merged = mergeStats(existing, username, newStats);
23078
+ if (hasInterventions) {
23079
+ merged.interventions = mergeInterventionStats(existing?.interventions, interventionDelta);
23080
+ }
23081
+ if (hasPromptTokens) {
23082
+ const pt = mergePromptTokenStats(existing?.prompts, existing?.tokens, promptTokenDelta);
23083
+ merged.prompts = pt.prompts;
23084
+ merged.tokens = pt.tokens;
23085
+ }
23086
+ if (hasDaily) {
23087
+ merged.daily = mergeDailyStats(existing?.daily, dailyDelta);
23088
+ }
23089
+ await writeFile(statsPath, YAML21.stringify(merged));
23090
+ filesToPush.push(`stats/${username}.yaml`);
23091
+ }
23092
+ try {
23093
+ if (await pathExists(getUserVotesDir())) {
23094
+ const { syncVotesToTeam: syncVotesToTeam2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
23095
+ const synced = await syncVotesToTeam2(writeRoot, username, getUserVotesDir());
23096
+ if (synced) {
23097
+ filesToPush.push(`votes/${username}.yaml`);
23098
+ }
23099
+ }
23100
+ } catch (e) {
23101
+ log.error(`Vote staging skipped: ${e.message}`);
23102
+ }
23103
+ };
22541
23104
  if (useReportsBranch && reportsConfig) {
22542
- const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22543
- writeRoot = await ensureReportsWorktree2(reportsConfig);
23105
+ let hasVotes = false;
23106
+ if (!hasStats && await pathExists(getUserVotesDir())) {
23107
+ const { hasPendingVoteDeltas: hasPendingVoteDeltas2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
23108
+ hasVotes = await hasPendingVoteDeltas2(getUserVotesDir(), username);
23109
+ }
23110
+ if (!hasStats && !hasVotes) {
23111
+ log.debug("No usage events or votes to report");
23112
+ return true;
23113
+ }
23114
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
23115
+ const pushed = await updateReports2(reportsConfig, async (wt) => {
23116
+ filesToPush.length = 0;
23117
+ await writeReportFiles(wt);
23118
+ return filesToPush.length > 0 ? { files: [...filesToPush], message: commitMsg } : null;
23119
+ }, { pushIfUnchanged: true });
23120
+ if (!pushed) {
23121
+ log.debug("Auto-report push was not confirmed; keeping local report data");
23122
+ await restoreStats?.();
23123
+ return false;
23124
+ }
22544
23125
  } else {
22545
23126
  const git = createGit2(repoPath);
22546
23127
  if (!await isDedicatedRepoRoot(repoPath)) {
@@ -22564,56 +23145,11 @@ async function reportUsageToTeam(repoPath, username, options) {
22564
23145
  await writeFile(yamlPath, pendingTeamConfig);
22565
23146
  }
22566
23147
  }
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;
23148
+ await writeReportFiles(repoPath);
23149
+ if (filesToPush.length === 0) {
23150
+ log.debug("No usage events or votes to report");
23151
+ return true;
22615
23152
  }
22616
- } else {
22617
23153
  await pushRepoDirectly(repoPath, commitMsg, filesToPush);
22618
23154
  }
22619
23155
  restoreStats = void 0;
@@ -22836,6 +23372,7 @@ __export(pull_exports, {
22836
23372
  compileClaudemd: () => compileClaudemd,
22837
23373
  compileCulture: () => compileCulture,
22838
23374
  compileRecallRulesBlock: () => compileRecallRulesBlock,
23375
+ filterAgentsByNamespaces: () => filterAgentsByNamespaces,
22839
23376
  filterRulesByKnowledgeNamespaces: () => filterRulesByKnowledgeNamespaces,
22840
23377
  injectRecallBlockIntoTools: () => injectRecallBlockIntoTools,
22841
23378
  pull: () => pull,
@@ -22913,58 +23450,9 @@ async function usageReportDisabled(repoPath) {
22913
23450
  return (await loadTeamConfig(repoPath))?.usageReport === false;
22914
23451
  }
22915
23452
  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]);
23453
+ const resolved = await resolveResourceNamespaces(localConfig);
23454
+ if (!resolved) return null;
23455
+ const { activeNamespaces, allSkillNamespaces } = resolved;
22968
23456
  const inactiveSkillNamespaces = [...allSkillNamespaces].filter((namespace) => !activeNamespaces.skills.includes(namespace));
22969
23457
  const activeSkillNames = /* @__PURE__ */ new Set();
22970
23458
  const inactiveSkillNames = /* @__PURE__ */ new Set();
@@ -22997,6 +23485,20 @@ function filterRulesByKnowledgeNamespaces(rules, knowledgeNamespaces) {
22997
23485
  return knowledgeNamespaces.includes(namespace);
22998
23486
  });
22999
23487
  }
23488
+ function filterAgentsByNamespaces(agents, agentNamespaces) {
23489
+ const kept = agentNamespaces ? agents.filter((agent) => !agent.namespace || agentNamespaces.includes(agent.namespace)) : agents;
23490
+ const seen = /* @__PURE__ */ new Map();
23491
+ for (const agent of kept) {
23492
+ const existing = seen.get(agent.name);
23493
+ if (existing) {
23494
+ throw new Error(
23495
+ `Duplicate agent "${agent.name}" found in active namespaces "${existing.namespace ?? "(root)"}" and "${agent.namespace ?? "(root)"}"`
23496
+ );
23497
+ }
23498
+ seen.set(agent.name, agent);
23499
+ }
23500
+ return kept;
23501
+ }
23000
23502
  async function scanRoleAwareSkills(localConfig, namespaces) {
23001
23503
  const items = /* @__PURE__ */ new Map();
23002
23504
  for (const namespace of namespaces.skills) {
@@ -23098,6 +23600,42 @@ async function getInstalledResourceTargets(teamConfig, localConfig) {
23098
23600
  }
23099
23601
  return targets.sort();
23100
23602
  }
23603
+ function tombstoneExtensions(type, tool) {
23604
+ if (type === "rules") return [.../* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"])];
23605
+ if (type === "agents") return AGENT_FILE_EXTENSIONS;
23606
+ return [""];
23607
+ }
23608
+ async function cleanupTombstonedResources(freshConfig, localConfig, scopeLabel) {
23609
+ const tombstoneTypes = [
23610
+ { type: "rules", toolPathField: "rules" },
23611
+ { type: "skills", toolPathField: "skills" },
23612
+ { type: "agents", toolPathField: "agents" }
23613
+ ];
23614
+ const baseDir = resolveBaseDir(localConfig);
23615
+ for (const { type, toolPathField } of tombstoneTypes) {
23616
+ const handler = getHandler(type);
23617
+ const tombstones = await handler.readTombstones(localConfig);
23618
+ if (tombstones.size === 0) continue;
23619
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
23620
+ const dir = toolPath[toolPathField];
23621
+ if (!dir) continue;
23622
+ if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
23623
+ if (isAgentExcluded(localConfig, tool)) continue;
23624
+ for (const name of tombstones) {
23625
+ for (const extension of tombstoneExtensions(type, tool)) {
23626
+ const localPath = path66.join(baseDir, dir, `${name}${extension}`);
23627
+ if (!await pathExists(localPath)) continue;
23628
+ if (type === "skills" && await hasVcsMetadataRecursive(localPath)) {
23629
+ 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.`);
23630
+ continue;
23631
+ }
23632
+ await remove(localPath);
23633
+ log.debug(`[${scopeLabel}] Cleaned up tombstoned ${type} ${name} from ${dir}`);
23634
+ }
23635
+ }
23636
+ }
23637
+ }
23638
+ }
23101
23639
  async function pullForScope(localConfig, options, policy = {}) {
23102
23640
  const scopeLabel = localConfig.scope;
23103
23641
  const revisionField = policy.revisionField ?? "lastPullRev";
@@ -23151,6 +23689,7 @@ async function pullForScope(localConfig, options, policy = {}) {
23151
23689
  } catch {
23152
23690
  }
23153
23691
  await injectRecallBlockIntoTools(freshConfig, localConfig, scopeLabel);
23692
+ await cleanupTombstonedResources(freshConfig, localConfig, scopeLabel);
23154
23693
  return;
23155
23694
  }
23156
23695
  log.debug(`[${scopeLabel}] Repo unchanged; resource target set changed, syncing`);
@@ -23223,6 +23762,11 @@ async function pullForScope(localConfig, options, policy = {}) {
23223
23762
  desiredSkillNames = new Set(items.map((i) => i.name));
23224
23763
  knownRepoSkillNames = new Set(allTeamSkills.map((i) => i.name));
23225
23764
  knownRepoSkillSources = new Map(allTeamSkills.map((i) => [i.name, i.sourcePath]));
23765
+ } else if (type === "agents") {
23766
+ items = filterAgentsByNamespaces(
23767
+ await handler.scanTeamForPull(freshConfig, localConfig),
23768
+ roleContext ? roleContext.activeNamespaces.agents : null
23769
+ );
23226
23770
  } else {
23227
23771
  items = await handler.scanTeamForPull(freshConfig, localConfig);
23228
23772
  }
@@ -23281,37 +23825,7 @@ async function pullForScope(localConfig, options, policy = {}) {
23281
23825
  totalSynced += items.length;
23282
23826
  }
23283
23827
  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
- }
23828
+ await cleanupTombstonedResources(freshConfig, localConfig, scopeLabel);
23315
23829
  if (roleContext) {
23316
23830
  await cleanupInactiveNamespaceSkills(
23317
23831
  freshConfig,
@@ -23320,6 +23834,11 @@ async function pullForScope(localConfig, options, policy = {}) {
23320
23834
  roleContext.inactiveSkillNames,
23321
23835
  roleContext.inactiveSkillSources
23322
23836
  );
23837
+ await getHandler("agents").cleanupInactiveNamespaces(
23838
+ freshConfig,
23839
+ localConfig,
23840
+ roleContext.activeNamespaces.agents
23841
+ );
23323
23842
  }
23324
23843
  }
23325
23844
  if (!options.dryRun && desiredSkillNames && knownRepoSkillNames) {
@@ -24034,11 +24553,11 @@ var init_pull = __esm({
24034
24553
  init_resources();
24035
24554
  init_base();
24036
24555
  init_rule_format();
24556
+ init_agent_format();
24037
24557
  init_tags();
24038
24558
  init_builtin_skills();
24039
24559
  init_types();
24040
- init_roles();
24041
- init_projects();
24560
+ init_resource_namespaces();
24042
24561
  init_home();
24043
24562
  init_update();
24044
24563
  init_learnings_mirror();
@@ -24773,10 +25292,12 @@ async function removeCore(type, names, options, localConfig, teamConfig) {
24773
25292
  log.info("Dry run \u2014 no changes made");
24774
25293
  return;
24775
25294
  }
24776
- const confirmed = await askConfirmation("Are you sure? [y/N] ");
24777
- if (!confirmed) {
24778
- log.info("Cancelled");
24779
- return;
25295
+ if (!options.force) {
25296
+ const confirmed = await askConfirmation("Are you sure? [y/N] ");
25297
+ if (!confirmed) {
25298
+ log.info("Cancelled");
25299
+ return;
25300
+ }
24780
25301
  }
24781
25302
  const spin = spinner(`Removing ${found.length} ${type}...`).start();
24782
25303
  let totalRemoved = 0;
@@ -24858,16 +25379,20 @@ var init_remove = __esm({
24858
25379
  // src/doctor.ts
24859
25380
  var doctor_exports = {};
24860
25381
  __export(doctor_exports, {
24861
- doctor: () => doctor
25382
+ buildChecks: () => buildChecks,
25383
+ doctor: () => doctor,
25384
+ resolveDoctorContext: () => resolveDoctorContext
24862
25385
  });
24863
25386
  import path70 from "path";
24864
- async function buildHookChecks(toolPaths, baseDir) {
25387
+ async function buildHookChecks(toolPaths, baseDir, localConfig) {
24865
25388
  const checks = [];
24866
25389
  for (const [tool, paths] of Object.entries(toolPaths)) {
24867
- if (!paths.settings) continue;
24868
- const settingsPath = path70.join(baseDir, paths.settings);
25390
+ const hookPath = paths.hooks ? path70.join(resolveToolBaseDir(tool, localConfig), paths.hooks) : paths.settings ? path70.join(baseDir, paths.settings) : void 0;
25391
+ if (!hookPath) continue;
25392
+ const settingsPath = hookPath;
24869
25393
  const parentDir = path70.dirname(settingsPath);
24870
- if (!await pathExists(parentDir)) continue;
25394
+ const installed = tool === COPILOT_TOOL_ID ? await isToolInstalledForConfig(tool, paths.hooks ?? paths.settings ?? "", localConfig) : await pathExists(parentDir);
25395
+ if (!installed) continue;
24871
25396
  checks.push({
24872
25397
  name: `teamai hooks in ${tool} settings`,
24873
25398
  check: async () => {
@@ -24894,21 +25419,20 @@ async function hasInstalledCodexHooks(toolPaths, baseDir) {
24894
25419
  }
24895
25420
  return false;
24896
25421
  }
24897
- async function doctor(options) {
24898
- log.info("Running diagnostics...\n");
25422
+ async function resolveDoctorContext() {
24899
25423
  const projectConfig = await detectProjectConfig();
24900
25424
  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);
24908
- }
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();
25425
+ if (!localConfig) return null;
25426
+ const teamConfig = await loadTeamConfig(localConfig.repo.localPath);
25427
+ const toolPaths = teamConfig ? Object.fromEntries(
25428
+ Object.entries(scopedToolPaths(teamConfig, localConfig)).filter(([tool]) => !isAgentExcluded(localConfig, tool))
25429
+ ) : {};
25430
+ const baseDir = resolveHookScope(localConfig).baseDir;
25431
+ return { localConfig, teamConfig, toolPaths, baseDir };
25432
+ }
25433
+ async function buildChecks(ctx) {
25434
+ const { localConfig, teamConfig, toolPaths, baseDir } = ctx;
25435
+ const providerName = teamConfig?.provider;
24912
25436
  const checks = [];
24913
25437
  if (providerName === "tgit") {
24914
25438
  const { isGfInstalled: isGfInstalled2, gfIsAuthenticated: gfIsAuthenticated2 } = await Promise.resolve().then(() => (init_tgit(), tgit_exports));
@@ -24954,34 +25478,24 @@ async function doctor(options) {
24954
25478
  });
24955
25479
  }
24956
25480
  checks.push(
24957
- {
24958
- name: `Local config exists (${configPathLabel})`,
24959
- check: async () => localConfig !== null,
24960
- fix: "Run `teamai init` to initialize"
24961
- },
24962
25481
  {
24963
25482
  name: "Team repo exists locally",
24964
- check: async () => {
24965
- if (!localConfig) return false;
24966
- return pathExists(localConfig.repo.localPath);
24967
- },
25483
+ check: async () => pathExists(localConfig.repo.localPath),
24968
25484
  fix: "Run `teamai init` to clone the team repo"
24969
25485
  },
24970
25486
  {
24971
25487
  name: "Team config (teamai.yaml) is valid",
24972
25488
  check: async () => {
24973
- if (!localConfig) return false;
24974
25489
  const config = await loadTeamConfig(localConfig.repo.localPath);
24975
25490
  return config !== null;
24976
25491
  },
24977
25492
  fix: "Check teamai.yaml in team repo for syntax errors"
24978
25493
  },
24979
- ...await buildHookChecks(toolPaths, baseDir),
25494
+ ...await buildHookChecks(toolPaths, baseDir, localConfig),
24980
25495
  {
24981
25496
  name: "Env variables injected in shell profile",
24982
25497
  check: async () => {
24983
25498
  if (teamConfig?.sharing?.env?.injectShellProfile === false) return true;
24984
- if (!localConfig) return true;
24985
25499
  const envYamlPath = path70.join(localConfig.repo.localPath, "env", "env.yaml");
24986
25500
  if (!await pathExists(envYamlPath)) return true;
24987
25501
  const home = getUserHome();
@@ -24999,28 +25513,80 @@ async function doctor(options) {
24999
25513
  fix: "Run `teamai pull` to inject env variables into shell profile"
25000
25514
  }
25001
25515
  );
25002
- let allPassed = true;
25516
+ return checks;
25517
+ }
25518
+ async function runChecks(checks, onResult) {
25519
+ const results = [];
25003
25520
  for (const { name, check, fix } of checks) {
25004
25521
  const ok = await check();
25005
- if (ok) {
25006
- console.log(` \u2714 ${name}`);
25522
+ const result = ok ? { name, ok } : { name, ok, fix };
25523
+ results.push(result);
25524
+ onResult?.(result);
25525
+ }
25526
+ return results;
25527
+ }
25528
+ function emitReport(report) {
25529
+ console.log(JSON.stringify(report, null, 2));
25530
+ }
25531
+ function renderResult({ name, ok, fix }) {
25532
+ if (ok) {
25533
+ console.log(` \u2714 ${name}`);
25534
+ return;
25535
+ }
25536
+ console.log(` \u2716 ${name}`);
25537
+ if (fix) console.log(` \u2192 ${fix}`);
25538
+ }
25539
+ async function doctor(options) {
25540
+ const jsonMode = options.json === true;
25541
+ if (jsonMode) setStderrOnly(true);
25542
+ log.info("Running diagnostics...\n");
25543
+ const ctx = await resolveDoctorContext();
25544
+ if (!ctx) {
25545
+ const notInitialized = {
25546
+ name: "TeamAI is not initialized",
25547
+ ok: false,
25548
+ fix: "Run `teamai init <repo-url>` in a project, or add `--scope user` for all projects"
25549
+ };
25550
+ if (jsonMode) {
25551
+ emitReport({ ok: false, scope: null, checks: [notInitialized] });
25007
25552
  } else {
25008
- console.log(` \u2716 ${name}`);
25009
- if (fix) console.log(` \u2192 ${fix}`);
25010
- allPassed = false;
25553
+ console.log(" Scope: not initialized\n");
25554
+ renderResult(notInitialized);
25555
+ console.log("");
25011
25556
  }
25557
+ log.warn("Initialization is required before diagnostics can run.");
25558
+ return false;
25012
25559
  }
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
- }
25560
+ const { localConfig, toolPaths, baseDir } = ctx;
25561
+ const scope = localConfig.scope ?? "user";
25562
+ if (!jsonMode) {
25563
+ const scopeLabel = `${scope}${scope === "project" && localConfig.projectRoot ? ` (${localConfig.projectRoot})` : ""}`;
25564
+ console.log(` Scope: ${scopeLabel}
25565
+ `);
25566
+ }
25567
+ const results = await runChecks(await buildChecks(ctx), jsonMode ? void 0 : renderResult);
25568
+ let allPassed = results.every((r) => r.ok);
25569
+ const { pkgDoctorReport: pkgDoctorReport2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
25570
+ const packageReport = await pkgDoctorReport2(localConfig, process.cwd());
25571
+ if (packageReport && !packageReport.allPassed) allPassed = false;
25572
+ const codexNote = await hasInstalledCodexHooks(toolPaths, baseDir) ? codexTrustReminder() : null;
25573
+ if (jsonMode) {
25574
+ emitReport({
25575
+ ok: allPassed,
25576
+ scope,
25577
+ checks: results,
25578
+ // pkgDoctorReport renders its own lines; they are human text, not checks.
25579
+ ...packageReport ? { packages: { ok: packageReport.allPassed, lines: packageReport.lines } } : {},
25580
+ ...codexNote ? { notes: [codexNote] } : {}
25581
+ });
25582
+ return allPassed;
25020
25583
  }
25021
- if (await hasInstalledCodexHooks(toolPaths, baseDir)) {
25584
+ if (packageReport) {
25585
+ for (const line of packageReport.lines) console.log(line);
25586
+ }
25587
+ if (codexNote) {
25022
25588
  console.log("");
25023
- log.info(codexTrustReminder());
25589
+ log.info(codexNote);
25024
25590
  }
25025
25591
  console.log("");
25026
25592
  if (allPassed) {
@@ -25028,6 +25594,7 @@ async function doctor(options) {
25028
25594
  } else {
25029
25595
  log.warn("Some checks failed. See suggestions above.");
25030
25596
  }
25597
+ return allPassed;
25031
25598
  }
25032
25599
  var init_doctor = __esm({
25033
25600
  "src/doctor.ts"() {
@@ -25036,6 +25603,7 @@ var init_doctor = __esm({
25036
25603
  init_fs();
25037
25604
  init_logger();
25038
25605
  init_types();
25606
+ init_base();
25039
25607
  init_hooks2();
25040
25608
  init_home();
25041
25609
  }
@@ -25152,7 +25720,8 @@ async function rolesInit(options) {
25152
25720
  description,
25153
25721
  resources: {
25154
25722
  knowledge: namespaces,
25155
- skills: namespaces
25723
+ skills: namespaces,
25724
+ agents: namespaces
25156
25725
  }
25157
25726
  });
25158
25727
  log.success(`Added role: ${id} (namespaces: ${namespaces.join(", ")})`);
@@ -25296,7 +25865,8 @@ async function rolesAdd(roleId, options) {
25296
25865
  description: options.description ?? "",
25297
25866
  resources: {
25298
25867
  knowledge: namespaces,
25299
- skills: namespaces
25868
+ skills: namespaces,
25869
+ agents: namespaces
25300
25870
  }
25301
25871
  };
25302
25872
  const updatedManifest = {
@@ -25410,7 +25980,8 @@ async function rolesUpdate(roleId, options) {
25410
25980
  description: hasDesc ? options.description : existingRole.description,
25411
25981
  resources: {
25412
25982
  knowledge: updatedNamespaces,
25413
- skills: updatedNamespaces
25983
+ skills: updatedNamespaces,
25984
+ agents: updatedNamespaces
25414
25985
  }
25415
25986
  };
25416
25987
  const updatedManifest = {
@@ -25616,6 +26187,12 @@ async function saveTagsScopeConfig(localConfig) {
25616
26187
  } else {
25617
26188
  await saveLocalConfig(localConfig);
25618
26189
  }
26190
+ try {
26191
+ const state = await loadStateForScope(localConfig);
26192
+ state.lastPullRev = null;
26193
+ await saveStateForScope(state, localConfig);
26194
+ } catch {
26195
+ }
25619
26196
  }
25620
26197
  async function tagsList(options) {
25621
26198
  const localConfig = await resolveTagsScope();
@@ -25833,10 +26410,13 @@ async function collectTeamRuleNames(repoPath) {
25833
26410
  async function collectTeamAgentNames(repoPath) {
25834
26411
  const teamAgentsDir = path74.join(repoPath, "agents");
25835
26412
  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
- );
26413
+ const names = /* @__PURE__ */ new Set();
26414
+ for (const { dir } of await listTeamAgentDirs(teamAgentsDir)) {
26415
+ for (const file of await listFiles(dir)) {
26416
+ if (file.endsWith(".yaml") || file.endsWith(".md")) names.add(file.replace(/\.(yaml|md)$/, ""));
26417
+ }
26418
+ }
26419
+ return names;
25840
26420
  }
25841
26421
  function isEmptyHooksResidue(parsed) {
25842
26422
  if (parsed == null || !("hooks" in parsed) || typeof parsed.hooks !== "object" || parsed.hooks == null) return false;
@@ -25851,7 +26431,7 @@ function opencodePluginTargets(baseDir, scope) {
25851
26431
  }
25852
26432
  return targets;
25853
26433
  }
25854
- async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, teamRuleNames, teamAgentNames, hookTargets, scope) {
26434
+ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, teamRuleNames, teamAgentNames, hookTargets, standaloneHookManifestPath, scope) {
25855
26435
  const res = {
25856
26436
  hookFiles: [],
25857
26437
  openclawHookDirs: [],
@@ -25861,7 +26441,16 @@ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, te
25861
26441
  ruleFiles: [],
25862
26442
  agentFiles: []
25863
26443
  };
25864
- if (tool === "opencode") {
26444
+ if (toolPath.hooks) {
26445
+ const hooksPath = path74.join(baseDir, toolPath.hooks);
26446
+ if (await pathExists(hooksPath) && (await hasTeamaiHooks(hooksPath, tool, standaloneHookManifestPath) || isEmptyHooksResidue(await readJson(hooksPath)))) {
26447
+ res.hookFiles.push({
26448
+ path: hooksPath,
26449
+ tool,
26450
+ manifestPath: standaloneHookManifestPath
26451
+ });
26452
+ }
26453
+ } else if (tool === "opencode") {
25865
26454
  const { resolveOpencodePluginDir: resolveOpencodePluginDir2, OPENCODE_HOOK_FILE: OPENCODE_HOOK_FILE2 } = await Promise.resolve().then(() => (init_opencode_hooks(), opencode_hooks_exports));
25866
26455
  for (const target of opencodePluginTargets(baseDir, scope)) {
25867
26456
  const pluginDir = resolveOpencodePluginDir2(target.baseDir, target.scope);
@@ -25936,8 +26525,8 @@ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, te
25936
26525
  const agentsDir = path74.join(baseDir, toolPath.agents);
25937
26526
  if (await pathExists(agentsDir)) {
25938
26527
  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)$/, "");
26528
+ const name = agentStemFromFilename(path74.basename(file));
26529
+ if (name === null) continue;
25941
26530
  if (!teamAgentNames.has(name) && !BUILTIN_AGENT_NAMES.has(name)) continue;
25942
26531
  res.agentFiles.push(path74.join(agentsDir, file));
25943
26532
  }
@@ -25948,6 +26537,10 @@ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, te
25948
26537
  async function buildRemovalPlan(localConfig, teamConfig, agentFilter) {
25949
26538
  const baseDir = resolveBaseDir(localConfig);
25950
26539
  const teamaiHome = getDataHome(localConfig);
26540
+ const standaloneHookManifestPath = getManagedHooksPath(
26541
+ localConfig.scope,
26542
+ localConfig.projectRoot
26543
+ );
25951
26544
  const repoPath = localConfig.repo.localPath;
25952
26545
  const teamSkillNames = await collectTeamSkillNames(repoPath);
25953
26546
  for (const name of BUILTIN_SKILL_NAMES) teamSkillNames.add(name);
@@ -25984,11 +26577,12 @@ async function buildRemovalPlan(localConfig, teamConfig, agentFilter) {
25984
26577
  await discoverToolResources(
25985
26578
  tool,
25986
26579
  toolPath,
25987
- baseDir,
26580
+ resolveToolBaseDir(tool, localConfig),
25988
26581
  teamSkillNames,
25989
26582
  teamRuleNames,
25990
26583
  teamAgentNames,
25991
26584
  hookTargets,
26585
+ standaloneHookManifestPath,
25992
26586
  localConfig.scope
25993
26587
  )
25994
26588
  );
@@ -26415,6 +27009,8 @@ var init_uninstall = __esm({
26415
27009
  init_types();
26416
27010
  init_builtin_rules();
26417
27011
  init_rule_format();
27012
+ init_agent_format();
27013
+ init_agents();
26418
27014
  init_builtin_agents();
26419
27015
  init_builtin_skills();
26420
27016
  init_fs();
@@ -26604,16 +27200,16 @@ async function hooksList(_options) {
26604
27200
  const { localConfig, teamConfig } = await autoDetectInit();
26605
27201
  const { baseDir } = resolveHookScope(localConfig);
26606
27202
  const rows = [];
26607
- for (const [tool, paths] of Object.entries(teamConfig.toolPaths)) {
26608
- if (!paths.settings) {
27203
+ for (const [tool, paths] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
27204
+ const hookPath = paths.hooks ? path76.join(resolveToolBaseDir(tool, localConfig), paths.hooks) : paths.settings ? path76.join(baseDir, paths.settings) : void 0;
27205
+ if (!hookPath) {
26609
27206
  rows.push({ tool, status: "not configured", settingsPath: "no settings configured" });
26610
27207
  continue;
26611
27208
  }
26612
- const settingsPath = path76.join(baseDir, paths.settings);
26613
27209
  rows.push({
26614
27210
  tool,
26615
- status: await getHookStatus(settingsPath, tool),
26616
- settingsPath: formatDisplayPath(settingsPath)
27211
+ status: await getHookStatus(hookPath, tool),
27212
+ settingsPath: formatDisplayPath(hookPath)
26617
27213
  });
26618
27214
  }
26619
27215
  console.log(formatHooksList(rows));
@@ -26642,6 +27238,18 @@ async function hooksRemove(_options) {
26642
27238
  const { localConfig, teamConfig } = await autoDetectInit();
26643
27239
  const { baseDir, manifestPath } = resolveHookScope(localConfig);
26644
27240
  await reconcileHooksToAllTools(teamConfig.toolPaths, baseDir, [], manifestPath, { removeAll: true });
27241
+ const copilotPaths = scopedToolPaths(teamConfig, localConfig)[COPILOT_TOOL_ID];
27242
+ if (copilotPaths?.hooks) {
27243
+ await reconcileHooks(
27244
+ path76.join(resolveToolBaseDir(COPILOT_TOOL_ID, localConfig), copilotPaths.hooks),
27245
+ COPILOT_TOOL_ID,
27246
+ [],
27247
+ {
27248
+ manifestPath: getManagedHooksPath(localConfig.scope, localConfig.projectRoot),
27249
+ removeAll: true
27250
+ }
27251
+ );
27252
+ }
26645
27253
  await sweepLegacyProjectHooks(teamConfig.toolPaths, localConfig);
26646
27254
  log.success("Hooks removed from all AI tool settings");
26647
27255
  }
@@ -26967,20 +27575,25 @@ async function saveSession(options) {
26967
27575
  if (usesReportsBranch(localConfig)) {
26968
27576
  const spin2 = spinner("Pushing session summary to team...").start();
26969
27577
  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);
27578
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
27579
+ let rel;
27580
+ let ran = false;
26979
27581
  const pushed = await withTimeout(
26980
- commitAndPushReports2(localConfig, commitMsg, [rel]),
27582
+ updateReports2(localConfig, async (wt) => {
27583
+ ran = true;
27584
+ const teamDir2 = path79.join(wt, "sessions", username);
27585
+ const written = await appendMonthlyLog(teamDir2, summary, { includePrompt: options.includePrompt });
27586
+ if (!written) return null;
27587
+ rel = path79.relative(wt, written);
27588
+ return { files: [rel], message: commitMsg };
27589
+ }),
26981
27590
  1e4,
26982
27591
  "Push timeout (10s)"
26983
27592
  );
27593
+ if (ran && !rel) {
27594
+ spin2.info("Session already present in the team log \u2014 nothing to push.");
27595
+ return;
27596
+ }
26984
27597
  if (pushed) spin2.succeed(`Pushed: ${rel}`);
26985
27598
  else spin2.info("Nothing new to push.");
26986
27599
  } catch (e) {
@@ -31104,11 +31717,13 @@ var init_hook_handlers = __esm({
31104
31717
  const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31105
31718
  if (usesReportsBranch2(localConfig)) {
31106
31719
  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`]);
31720
+ const { hasPendingVoteDeltas: hasPendingVoteDeltas2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
31721
+ if (await hasPendingVoteDeltas2(votesDir, localConfig.username)) {
31722
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
31723
+ await updateReports2(localConfig, async (wt) => await syncVotesToTeam2(wt, localConfig.username, votesDir) ? {
31724
+ files: [`votes/${localConfig.username}.yaml`],
31725
+ message: `[teamai] Update votes for ${localConfig.username}`
31726
+ } : null);
31112
31727
  }
31113
31728
  } catch {
31114
31729
  }
@@ -34828,7 +35443,7 @@ var init_codebase = __esm({
34828
35443
 
34829
35444
  // src/wiki-engine/code-knowledge/code-collector.ts
34830
35445
  import { createHash as createHash5 } from "crypto";
34831
- import { execFile as execFile4 } from "child_process";
35446
+ import { execFile as execFile3 } from "child_process";
34832
35447
  import { readFile as readFile6, readdir as readdir2, stat as stat2 } from "fs/promises";
34833
35448
  import path105 from "path";
34834
35449
  import { promisify as promisify3 } from "util";
@@ -34984,7 +35599,7 @@ var init_code_collector = __esm({
34984
35599
  "src/wiki-engine/code-knowledge/code-collector.ts"() {
34985
35600
  "use strict";
34986
35601
  init_wiki_protocol();
34987
- execFileAsync3 = promisify3(execFile4);
35602
+ execFileAsync3 = promisify3(execFile3);
34988
35603
  KEY_FILE_PATTERNS = {
34989
35604
  go: [/main\.go$/, /cmd\/.*\.go$/, /handler.*\.go$/, /server\.go$/, /router\.go$/],
34990
35605
  python: [/main\.py$/, /app\.py$/, /server\.py$/, /routes?\.py$/, /models?\.py$/],
@@ -42953,7 +43568,7 @@ program.name("teamai").description("TeamAI \u2014 Make Every Team AI Native").ve
42953
43568
  process.exit(1);
42954
43569
  }
42955
43570
  });
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) => {
43571
+ 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
43572
  const globalOpts = program.opts();
42958
43573
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42959
43574
  await init2({ ...globalOpts, ...cmdOpts, repoPositional: repoArg });
@@ -43024,16 +43639,17 @@ membersCmd.command("list").description("List team members").action(async () => {
43024
43639
  const { listMembers: listMembers2 } = await Promise.resolve().then(() => (init_members(), members_exports));
43025
43640
  await listMembers2(globalOpts);
43026
43641
  });
43027
- program.command("remove <type> <names...>").description("Remove resource(s) from team repo and all local AI tools (type: skills|rules|agents|mcp)").action(async (type, names) => {
43642
+ program.command("remove <type> <names...>").description("Remove resource(s) from team repo and all local AI tools (type: skills|rules|agents|mcp)").option("--force", "Skip confirmation prompt").action(async (type, names, cmdOpts) => {
43028
43643
  const globalOpts = program.opts();
43029
43644
  const { remove: remove3 } = await Promise.resolve().then(() => (init_remove(), remove_exports));
43030
- await remove3(type, names, globalOpts);
43645
+ await remove3(type, names, { ...globalOpts, ...cmdOpts });
43031
43646
  });
43032
43647
  registerPackagesCommand(program);
43033
- program.command("doctor").description("Diagnose configuration issues").action(async () => {
43648
+ program.command("doctor").description("Diagnose configuration issues").option("--json", "Output the report as JSON (suitable for CI)").action(async (cmdOpts) => {
43034
43649
  const globalOpts = program.opts();
43035
43650
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
43036
- await doctor2(globalOpts);
43651
+ const allPassed = await doctor2({ ...globalOpts, ...cmdOpts });
43652
+ if (!allPassed) process.exitCode = 1;
43037
43653
  });
43038
43654
  var rolesCmd = program.command("roles").description("Manage team roles and resource namespaces").action(async () => {
43039
43655
  const globalOpts = program.opts();
@@ -43334,7 +43950,7 @@ program.command("todowrite-hint", { hidden: true }).description("Remind the agen
43334
43950
  await todoWriteHint2();
43335
43951
  }
43336
43952
  });
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) => {
43953
+ 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
43954
  const globalOpts = program.opts();
43339
43955
  if (cmdOpts.cacheStatus || cmdOpts.cacheGc) {
43340
43956
  const { cacheCmd: cacheCmd2 } = await Promise.resolve().then(() => (init_cache_cmd(), cache_cmd_exports));