teamai-cli 0.24.0 → 0.25.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -559,6 +559,7 @@ __export(types_exports, {
559
559
  getDataHome: () => getDataHome,
560
560
  getEnvBackupPath: () => getEnvBackupPath,
561
561
  getHooksSharing: () => getHooksSharing,
562
+ getInterventionSharing: () => getInterventionSharing,
562
563
  getKnowledgeDir: () => getKnowledgeDir,
563
564
  getManagedHooksPath: () => getManagedHooksPath,
564
565
  getMcpSharing: () => getMcpSharing,
@@ -590,11 +591,15 @@ __export(types_exports, {
590
591
  resolveHookScope: () => resolveHookScope,
591
592
  resolveLegacyProjectHookScope: () => resolveLegacyProjectHookScope,
592
593
  scopedToolPaths: () => scopedToolPaths,
593
- totalTokens: () => totalTokens
594
+ totalTokens: () => totalTokens,
595
+ usesReportsBranch: () => usesReportsBranch
594
596
  });
595
597
  import { z } from "zod";
596
598
  import path3 from "path";
597
599
  import { createHash } from "crypto";
600
+ function getInterventionSharing(config) {
601
+ return { correctionKeywords: config.sharing?.intervention?.correctionKeywords ?? [] };
602
+ }
598
603
  function getHooksSharing(config) {
599
604
  const h = config.sharing?.hooks;
600
605
  return {
@@ -722,6 +727,9 @@ function scopedToolPaths(teamConfig, localConfig) {
722
727
  function isSelfMode(localConfig) {
723
728
  return localConfig.repo.kind === "self";
724
729
  }
730
+ function usesReportsBranch(localConfig) {
731
+ return localConfig.repo.kind !== "http";
732
+ }
725
733
  function getKnowledgeDir(localConfig) {
726
734
  return localConfig.repo.localPath;
727
735
  }
@@ -730,10 +738,13 @@ function getDataHome(localConfig) {
730
738
  return getTeamaiHome(localConfig.scope, localConfig.projectRoot);
731
739
  }
732
740
  function getReportsDir(localConfig) {
741
+ if (!usesReportsBranch(localConfig)) {
742
+ return localConfig.repo.localPath;
743
+ }
733
744
  if (isSelfMode(localConfig)) {
734
745
  return path3.join(localConfig.repo.localPath, REPORTS_WORKTREE_DIRNAME);
735
746
  }
736
- return localConfig.repo.localPath;
747
+ return path3.join(path3.dirname(localConfig.repo.localPath), REPORTS_WORKTREE_DIRNAME);
737
748
  }
738
749
  function getTeamaiHome(scope, projectRoot) {
739
750
  if (scope === "project") {
@@ -872,6 +883,15 @@ var init_types = __esm({
872
883
  allowedCommands: z.array(z.string()).default([]),
873
884
  /** Allowed http/sse hosts (supports a leading `*.` wildcard). Empty = no restriction. */
874
885
  allowedHosts: z.array(z.string()).default([])
886
+ }).optional(),
887
+ // Optional (not .default) so existing TeamaiConfig literals stay valid; use
888
+ // getInterventionSharing() for the defaulted view.
889
+ intervention: z.object({
890
+ /** Extra course-correction keywords, merged with the built-in list
891
+ * (CORRECTION_KEYWORDS). Teams add the words their members actually type,
892
+ * e.g. Spanish "rehazlo" or "no era eso". Matched case-insensitively; a
893
+ * keyword in a space-separated script must appear as a whole word. */
894
+ correctionKeywords: z.array(z.string()).default([])
875
895
  }).optional()
876
896
  });
877
897
  SourceConfigSchema = z.object({
@@ -948,6 +968,19 @@ var init_types = __esm({
948
968
  mcp: ".qoder/settings.json",
949
969
  mcpProject: ".qoder/settings.json"
950
970
  },
971
+ // Kiro: skills, steering (rules), and custom agents sync to .kiro/. Kiro CLI
972
+ // 2.x stores lifecycle hooks inside each .kiro/agents/*.json config. The
973
+ // Kiro agent renderer therefore embeds TeamAI's session-start dispatch as
974
+ // `hooks.agentSpawn`; there is no standalone `settings` hook surface.
975
+ // MCP uses the dedicated, mcpServers-only .kiro/settings/mcp.json:
976
+ // https://kiro.dev/docs/mcp/configuration/
977
+ kiro: {
978
+ skills: ".kiro/skills",
979
+ rules: ".kiro/steering",
980
+ agents: ".kiro/agents",
981
+ mcp: ".kiro/settings/mcp.json",
982
+ mcpProject: ".kiro/settings/mcp.json"
983
+ },
951
984
  // ZCode: user-level config lives at ~/.zcode/cli/config.json (a shared file
952
985
  // that also carries plugin state — reconcile must merge, never replace).
953
986
  // Hooks are Claude-shaped but nested under `hooks.events` and gated by
@@ -1009,6 +1042,8 @@ var init_types = __esm({
1009
1042
  * Knowledge lives on main under <businessRepoRoot>/.teamai/;
1010
1043
  * reports (members/sessions/votes/stats) live on the
1011
1044
  * `teamai-reports` orphan branch. localPath = <businessRepoRoot>/.teamai.
1045
+ * Independent git clones (`kind: 'git'` or omitted) use the same reports
1046
+ * branch; the worktree sits beside the clone, not inside it.
1012
1047
  */
1013
1048
  kind: z.enum(["git", "http", "self"]).optional(),
1014
1049
  /** Base URL of the HTTP team repo (only when kind === 'http'). */
@@ -1212,9 +1247,178 @@ var init_types = __esm({
1212
1247
  }
1213
1248
  });
1214
1249
 
1250
+ // src/roles.ts
1251
+ var roles_exports = {};
1252
+ __export(roles_exports, {
1253
+ activeRoleIds: () => activeRoleIds,
1254
+ describeRoles: () => describeRoles,
1255
+ findRole: () => findRole,
1256
+ listRoleIds: () => listRoleIds,
1257
+ loadRolesManifest: () => loadRolesManifest,
1258
+ matchesRoles: () => matchesRoles,
1259
+ resolveRoleResourceNamespaces: () => resolveRoleResourceNamespaces,
1260
+ saveRolesManifest: () => saveRolesManifest,
1261
+ warnUnknownRoleIds: () => warnUnknownRoleIds
1262
+ });
1263
+ import path4 from "path";
1264
+ import YAML from "yaml";
1265
+ import { z as z2 } from "zod";
1266
+ function validateManifestShape(raw) {
1267
+ if (!raw || typeof raw !== "object") {
1268
+ throw new Error("Invalid roles manifest: expected an object");
1269
+ }
1270
+ const candidate = raw;
1271
+ const roles = candidate.roles;
1272
+ if (!Array.isArray(roles) || roles.length === 0) {
1273
+ throw new Error("Invalid roles manifest: roles must be a non-empty array");
1274
+ }
1275
+ for (const role of roles) {
1276
+ if (!role || typeof role !== "object") {
1277
+ throw new Error("Invalid roles manifest: every role must be an object");
1278
+ }
1279
+ const resources = role.resources;
1280
+ if (!resources || typeof resources !== "object" || Array.isArray(resources)) {
1281
+ throw new Error(`Invalid roles manifest: role ${role.id ?? "<unknown>"} is missing resources`);
1282
+ }
1283
+ const ALLOWED_RESOURCE_KEYS = /* @__PURE__ */ new Set([...ROLE_RESOURCE_TYPES, "learnings"]);
1284
+ for (const key of Object.keys(resources)) {
1285
+ if (!ALLOWED_RESOURCE_KEYS.has(key)) {
1286
+ throw new Error(`Invalid roles manifest: unknown resource type "${key}"`);
1287
+ }
1288
+ }
1289
+ }
1290
+ const manifest = RolesManifestSchema.parse(raw);
1291
+ const ids = /* @__PURE__ */ new Set();
1292
+ for (const role of manifest.roles) {
1293
+ if (ids.has(role.id)) {
1294
+ throw new Error(`Invalid roles manifest: duplicate role id "${role.id}"`);
1295
+ }
1296
+ ids.add(role.id);
1297
+ }
1298
+ return manifest;
1299
+ }
1300
+ async function loadRolesManifest(repoPath) {
1301
+ const manifestPath = path4.join(repoPath, "manifest", "roles.yaml");
1302
+ const content = await readFileSafe(manifestPath);
1303
+ if (!content) {
1304
+ throw new Error(`Roles manifest not found: ${manifestPath}`);
1305
+ }
1306
+ let raw;
1307
+ try {
1308
+ raw = YAML.parse(content);
1309
+ } catch (error) {
1310
+ throw new Error(`Invalid roles manifest YAML: ${error.message}`);
1311
+ }
1312
+ return validateManifestShape(raw);
1313
+ }
1314
+ async function saveRolesManifest(repoPath, manifest) {
1315
+ validateManifestShape(manifest);
1316
+ const manifestDir = path4.join(repoPath, "manifest");
1317
+ const manifestPath = path4.join(manifestDir, "roles.yaml");
1318
+ await ensureDir(manifestDir);
1319
+ await writeFile(manifestPath, YAML.stringify(manifest));
1320
+ }
1321
+ function findRole(manifest, roleId) {
1322
+ return manifest.roles.find((candidate) => candidate.id === roleId);
1323
+ }
1324
+ function listRoleIds(manifest) {
1325
+ return manifest.roles.map((role) => role.id);
1326
+ }
1327
+ function describeRoles(roles) {
1328
+ return roles.map((role) => role.description ? `${role.id}: ${role.description}` : `${role.id}`);
1329
+ }
1330
+ function getRoleOrThrow(manifest, roleId) {
1331
+ const role = manifest.roles.find((candidate) => candidate.id === roleId);
1332
+ if (!role) {
1333
+ throw new Error(`Unknown role "${roleId}". Valid roles: ${listRoleIds(manifest).join(", ")}`);
1334
+ }
1335
+ return role;
1336
+ }
1337
+ function resolveRoleResourceNamespaces(input) {
1338
+ const resolvedRoles = [
1339
+ getRoleOrThrow(input.manifest, input.primaryRole),
1340
+ ...input.additionalRoles.map((roleId) => getRoleOrThrow(input.manifest, roleId))
1341
+ ];
1342
+ const namespaces = {
1343
+ knowledge: [],
1344
+ skills: [],
1345
+ // Roles never contribute learnings namespaces; only projects do. Kept empty
1346
+ // so the shape matches project resolution for a clean union at the call site.
1347
+ learnings: [],
1348
+ agents: []
1349
+ };
1350
+ for (const type of ROLE_RESOURCE_TYPES) {
1351
+ const seen = /* @__PURE__ */ new Set();
1352
+ for (const role of resolvedRoles) {
1353
+ for (const namespace of role.resources[type]) {
1354
+ if (seen.has(namespace)) continue;
1355
+ seen.add(namespace);
1356
+ namespaces[type].push(namespace);
1357
+ }
1358
+ }
1359
+ }
1360
+ return namespaces;
1361
+ }
1362
+ function activeRoleIds(localConfig) {
1363
+ if (!localConfig.primaryRole) return null;
1364
+ return [.../* @__PURE__ */ new Set([localConfig.primaryRole, ...localConfig.additionalRoles ?? []])];
1365
+ }
1366
+ function matchesRoles(entryRoles, active) {
1367
+ if (!entryRoles || active == null) return true;
1368
+ return entryRoles.some((role) => active.includes(role));
1369
+ }
1370
+ async function warnUnknownRoleIds(repoPath, file, entries) {
1371
+ if (!entries.some((entry) => entry.roles && entry.roles.length > 0)) return;
1372
+ let known;
1373
+ try {
1374
+ known = new Set(listRoleIds(await loadRolesManifest(repoPath)));
1375
+ } catch {
1376
+ return;
1377
+ }
1378
+ for (const entry of entries) {
1379
+ for (const role of entry.roles ?? []) {
1380
+ if (known.has(role) || reportedUnknownRoles.has(`${file}:${role}`)) continue;
1381
+ reportedUnknownRoles.add(`${file}:${role}`);
1382
+ log.warn(`roles: unknown role id "${role}" in ${file} ${entry.kind} "${entry.name}". Valid roles: ${[...known].join(", ")}`);
1383
+ }
1384
+ }
1385
+ }
1386
+ var ROLE_RESOURCE_TYPES, RoleResourceNamespacesSchema, RoleSchema, RolesManifestSchema, reportedUnknownRoles;
1387
+ var init_roles = __esm({
1388
+ "src/roles.ts"() {
1389
+ "use strict";
1390
+ init_fs();
1391
+ init_logger();
1392
+ ROLE_RESOURCE_TYPES = ["knowledge", "skills", "agents"];
1393
+ RoleResourceNamespacesSchema = z2.object({
1394
+ knowledge: z2.array(z2.string().min(1)),
1395
+ skills: z2.array(z2.string().min(1)),
1396
+ // Optional: a role without `agents` receives root-level agents only, which
1397
+ // is what every manifest written before this key existed already got.
1398
+ agents: z2.array(z2.string().min(1)).default([]),
1399
+ // learnings is accepted for backward compatibility but ignored at runtime.
1400
+ // All learnings are shared flat across the entire team (no namespace isolation).
1401
+ learnings: z2.array(z2.string()).optional()
1402
+ });
1403
+ RoleSchema = z2.object({
1404
+ id: z2.string().min(1),
1405
+ description: z2.string().default(""),
1406
+ resources: RoleResourceNamespacesSchema
1407
+ });
1408
+ RolesManifestSchema = z2.object({
1409
+ version: z2.number(),
1410
+ roles: z2.array(RoleSchema).min(1),
1411
+ // defaults.shareTarget was removed: learnings are flat, no namespace routing needed.
1412
+ // Old manifests with a defaults block are still parseable (z.passthrough on object level).
1413
+ defaults: z2.object({}).passthrough().optional()
1414
+ });
1415
+ reportedUnknownRoles = /* @__PURE__ */ new Set();
1416
+ }
1417
+ });
1418
+
1215
1419
  // src/bundled-runtime.ts
1216
1420
  import fs2 from "fs";
1217
- import path4 from "path";
1421
+ import path5 from "path";
1218
1422
  function compareSemver(a, b) {
1219
1423
  const aParts = a.split(".").map((s) => parseInt(s, 10) || 0);
1220
1424
  const bParts = b.split(".").map((s) => parseInt(s, 10) || 0);
@@ -1230,18 +1434,18 @@ function pickLatestVersion(versions) {
1230
1434
  return versions.reduce((best, v) => compareSemver(v, best) > 0 ? v : best, versions[0]);
1231
1435
  }
1232
1436
  function latestVersionDir(relDir) {
1233
- const versionsDir = path4.join(getUserHome(), relDir);
1437
+ const versionsDir = path5.join(getUserHome(), relDir);
1234
1438
  try {
1235
1439
  const versions = fs2.readdirSync(versionsDir).filter((d) => !d.startsWith("."));
1236
1440
  const latest = pickLatestVersion(versions);
1237
- return latest ? path4.join(versionsDir, latest) : null;
1441
+ return latest ? path5.join(versionsDir, latest) : null;
1238
1442
  } catch {
1239
1443
  return null;
1240
1444
  }
1241
1445
  }
1242
1446
  function resolveWorkbuddyNode() {
1243
1447
  const dir = latestVersionDir(WORKBUDDY_BUNDLED_NODE_DIR);
1244
- const nodeBin = dir && path4.join(dir, "bin", "node");
1448
+ const nodeBin = dir && path5.join(dir, "bin", "node");
1245
1449
  return nodeBin && fs2.existsSync(nodeBin) ? nodeBin : null;
1246
1450
  }
1247
1451
  function resolveCodebuddyNode() {
@@ -1251,10 +1455,10 @@ function resolveCodebuddyNode() {
1251
1455
  for (const entry of entries) {
1252
1456
  if (!entry.startsWith(".codebuddy-server")) continue;
1253
1457
  try {
1254
- const binDir = path4.join(home, entry, "bin");
1458
+ const binDir = path5.join(home, entry, "bin");
1255
1459
  const stableDirs = fs2.readdirSync(binDir).filter((d) => d.startsWith("stable-"));
1256
1460
  for (const stable of stableDirs) {
1257
- const nodeBin = path4.join(binDir, stable, "node");
1461
+ const nodeBin = path5.join(binDir, stable, "node");
1258
1462
  if (fs2.existsSync(nodeBin)) return nodeBin;
1259
1463
  }
1260
1464
  } catch {
@@ -1270,8 +1474,8 @@ function resolveWorkbuddyShell() {
1270
1474
  if (process.platform === "win32") {
1271
1475
  const dir = latestVersionDir(WORKBUDDY_PORTABLE_GIT_DIR);
1272
1476
  if (dir) {
1273
- for (const rel of ["bin", path4.join("usr", "bin")]) {
1274
- const shBin = path4.join(dir, rel, "sh.exe");
1477
+ for (const rel of ["bin", path5.join("usr", "bin")]) {
1478
+ const shBin = path5.join(dir, rel, "sh.exe");
1275
1479
  if (fs2.existsSync(shBin)) {
1276
1480
  _wbShellCache = shBin;
1277
1481
  break;
@@ -1301,7 +1505,7 @@ var init_bundled_runtime = __esm({
1301
1505
 
1302
1506
  // src/builtin-hooks.ts
1303
1507
  import fs3 from "fs";
1304
- import path5 from "path";
1508
+ import path6 from "path";
1305
1509
  import { fileURLToPath } from "url";
1306
1510
  function hasShell() {
1307
1511
  if (_hasShellCache === void 0) {
@@ -1316,8 +1520,8 @@ function hasShell() {
1316
1520
  function resolveTeamaiEntryScript() {
1317
1521
  try {
1318
1522
  const thisFile = fileURLToPath(import.meta.url);
1319
- const distDir = path5.dirname(thisFile);
1320
- const candidate = path5.join(distDir, "index.js");
1523
+ const distDir = path6.dirname(thisFile);
1524
+ const candidate = path6.join(distDir, "index.js");
1321
1525
  if (fs3.existsSync(candidate)) return candidate;
1322
1526
  } catch {
1323
1527
  }
@@ -1328,8 +1532,8 @@ function ensureTeamaiWrapper() {
1328
1532
  if (!entryScript) return null;
1329
1533
  const nodeBin = resolveWorkbuddyNode() ?? resolveCodebuddyNode() ?? process.argv[0];
1330
1534
  const home = getUserHome();
1331
- const binDir = path5.join(home, TEAMAI_BIN_DIR);
1332
- const wrapperPath = path5.join(binDir, WRAPPER_NAME);
1535
+ const binDir = path6.join(home, TEAMAI_BIN_DIR);
1536
+ const wrapperPath = path6.join(binDir, WRAPPER_NAME);
1333
1537
  const script = [
1334
1538
  "#!/bin/sh",
1335
1539
  `# Auto-generated by teamai \u2014 do not edit.`,
@@ -1383,7 +1587,7 @@ function getWrapperDispatchCommand(event, tool, matcher) {
1383
1587
  return `PATH="$HOME/${TEAMAI_BIN_DIR}:$PATH" teamai hook-dispatch ${event} --tool ${tool}${matcherArg} 2>/dev/null || true`;
1384
1588
  }
1385
1589
  function builtinHookDefs(tool) {
1386
- const withTimeout3 = tool === "cursor" || tool === "workbuddy" || tool === "codebuddy" || tool === "zcode";
1590
+ const withTimeout3 = tool === "cursor" || tool === "workbuddy" || tool === "codebuddy";
1387
1591
  const buildCommand = tool === "zcode" ? getRawDispatchCommand : WRAPPER_TOOLS.has(tool) ? getWrapperDispatchCommand : getDispatchCommand;
1388
1592
  return BUILTIN_HOOK_SPECS.map((spec) => ({
1389
1593
  source: "builtin",
@@ -1427,7 +1631,7 @@ var init_builtin_hooks = __esm({
1427
1631
  });
1428
1632
 
1429
1633
  // src/resources/base.ts
1430
- import path6 from "path";
1634
+ import path7 from "path";
1431
1635
  function toolInstallRoot(toolPath) {
1432
1636
  const segments = toolPath.split("/");
1433
1637
  if (segments[0] === ".config" && segments.length > 1) {
@@ -1451,7 +1655,7 @@ var init_base = __esm({
1451
1655
  */
1452
1656
  static async isToolInstalled(toolPath, baseDir) {
1453
1657
  const base = baseDir ?? getUserHome();
1454
- const toolRoot = path6.join(base, toolInstallRoot(toolPath));
1658
+ const toolRoot = path7.join(base, toolInstallRoot(toolPath));
1455
1659
  return pathExists(toolRoot);
1456
1660
  }
1457
1661
  /**
@@ -1459,7 +1663,7 @@ var init_base = __esm({
1459
1663
  * Returns a Set of resource names that have been explicitly deleted.
1460
1664
  */
1461
1665
  async readTombstones(localConfig) {
1462
- const tombstonePath = path6.join(localConfig.repo.localPath, this.type, TOMBSTONE_FILE);
1666
+ const tombstonePath = path7.join(localConfig.repo.localPath, this.type, TOMBSTONE_FILE);
1463
1667
  const content = await readFileSafe(tombstonePath);
1464
1668
  if (!content) return /* @__PURE__ */ new Set();
1465
1669
  return new Set(
@@ -1470,9 +1674,9 @@ var init_base = __esm({
1470
1674
  * Append a resource name to the tombstone file, deduplicating and sorting.
1471
1675
  */
1472
1676
  async addTombstone(name, localConfig) {
1473
- const dir = path6.join(localConfig.repo.localPath, this.type);
1677
+ const dir = path7.join(localConfig.repo.localPath, this.type);
1474
1678
  await ensureDir(dir);
1475
- const tombstonePath = path6.join(dir, TOMBSTONE_FILE);
1679
+ const tombstonePath = path7.join(dir, TOMBSTONE_FILE);
1476
1680
  const existing = await this.readTombstones(localConfig);
1477
1681
  existing.add(name);
1478
1682
  const sorted = [...existing].sort();
@@ -1495,17 +1699,17 @@ var init_base = __esm({
1495
1699
  });
1496
1700
 
1497
1701
  // src/resources/hooks.ts
1498
- import path7 from "path";
1499
- import { z as z2 } from "zod";
1500
- import YAML from "yaml";
1702
+ import path8 from "path";
1703
+ import { z as z3 } from "zod";
1704
+ import YAML2 from "yaml";
1501
1705
  function teamHooksYamlPath(repoPath) {
1502
- return path7.join(repoPath, "hooks", "hooks.yaml");
1706
+ return path8.join(repoPath, "hooks", "hooks.yaml");
1503
1707
  }
1504
1708
  async function parseHooksYaml(repoPath) {
1505
1709
  const content = await readFileSafe(teamHooksYamlPath(repoPath));
1506
1710
  if (!content) return null;
1507
1711
  try {
1508
- return HooksYamlSchema.parse(YAML.parse(content));
1712
+ return HooksYamlSchema.parse(YAML2.parse(content));
1509
1713
  } catch (e) {
1510
1714
  log.warn(`Invalid hooks.yaml format: ${e.message} \u2014 skipping team hooks this run`);
1511
1715
  return null;
@@ -1520,7 +1724,8 @@ function teamHookToDef(h) {
1520
1724
  command: h.command,
1521
1725
  timeout: h.timeout,
1522
1726
  description: `${TEAMAI_CUSTOM_HOOK_PREFIX}${h.id}] ${h.description}`,
1523
- tools: h.tools
1727
+ tools: h.tools,
1728
+ roles: h.roles
1524
1729
  };
1525
1730
  }
1526
1731
  async function parseTeamHooks(repoPath) {
@@ -1544,6 +1749,8 @@ async function resolveTeamHooks(teamConfig, repoPath, opts = {}) {
1544
1749
  if (defs.length > 0) log.warn(`Team hooks disabled (TEAMAI_HOOKS_DISABLED) \u2014 skipping ${defs.length} team hook(s)`);
1545
1750
  return { defs: [], builtin };
1546
1751
  }
1752
+ await warnUnknownRoleIds(repoPath, "hooks.yaml", defs.map((d) => ({ kind: "hook", name: d.key, roles: d.roles })));
1753
+ defs = defs.filter((d) => matchesRoles(d.roles, opts.activeRoles));
1547
1754
  if (sharing.requireTeamScripts) {
1548
1755
  const before = defs.length;
1549
1756
  defs = defs.filter((d) => isTeamScriptCommand(d.command));
@@ -1570,28 +1777,31 @@ var init_hooks = __esm({
1570
1777
  init_types();
1571
1778
  init_fs();
1572
1779
  init_logger();
1573
- TeamHookSchema = z2.object({
1780
+ init_roles();
1781
+ TeamHookSchema = z3.object({
1574
1782
  /** Unique id (marker + manifest index). */
1575
- id: z2.string().regex(/^[a-z0-9-]+$/),
1783
+ id: z3.string().regex(/^[a-z0-9-]+$/),
1576
1784
  /** Written into the hook description. */
1577
- description: z2.string(),
1785
+ description: z3.string(),
1578
1786
  /** Claude PascalCase event name. */
1579
- event: z2.string().min(1),
1787
+ event: z3.string().min(1),
1580
1788
  /** Optional tool matcher (e.g. "Bash"). */
1581
- matcher: z2.string().optional(),
1789
+ matcher: z3.string().optional(),
1582
1790
  /** Shell command to run. */
1583
- command: z2.string().min(1),
1791
+ command: z3.string().min(1),
1584
1792
  /** Optional per-hook timeout in seconds. */
1585
- timeout: z2.number().optional(),
1793
+ timeout: z3.number().optional(),
1586
1794
  /** Optional restriction to specific tools (default = all hook-capable tools). */
1587
- tools: z2.array(z2.string()).optional()
1795
+ tools: z3.array(z3.string()).optional(),
1796
+ /** Optional restriction to members holding one of these role ids (default = every member). */
1797
+ roles: z3.array(z3.string()).optional()
1588
1798
  });
1589
- BuiltinOverrideSchema = z2.object({
1590
- disabled: z2.array(z2.string()).default([]),
1591
- overrides: z2.record(z2.string(), z2.object({ timeout: z2.number().optional() })).default({})
1799
+ BuiltinOverrideSchema = z3.object({
1800
+ disabled: z3.array(z3.string()).default([]),
1801
+ overrides: z3.record(z3.string(), z3.object({ timeout: z3.number().optional() })).default({})
1592
1802
  }).default({ disabled: [], overrides: {} });
1593
- HooksYamlSchema = z2.object({
1594
- hooks: z2.array(TeamHookSchema).default([]),
1803
+ HooksYamlSchema = z3.object({
1804
+ hooks: z3.array(TeamHookSchema).default([]),
1595
1805
  builtin: BuiltinOverrideSchema
1596
1806
  });
1597
1807
  HooksHandler = class extends ResourceHandler {
@@ -1635,10 +1845,10 @@ __export(opencode_hooks_exports, {
1635
1845
  removeOpencodeHooks: () => removeOpencodeHooks,
1636
1846
  resolveOpencodePluginDir: () => resolveOpencodePluginDir
1637
1847
  });
1638
- import path8 from "path";
1848
+ import path9 from "path";
1639
1849
  function resolveOpencodePluginDir(baseDir, scope) {
1640
- const configDir = scope === "project" ? ".opencode" : path8.join(".config", "opencode");
1641
- return path8.join(baseDir, configDir, OPENCODE_PLUGIN_DIR);
1850
+ const configDir = scope === "project" ? ".opencode" : path9.join(".config", "opencode");
1851
+ return path9.join(baseDir, configDir, OPENCODE_PLUGIN_DIR);
1642
1852
  }
1643
1853
  function buildPluginSource() {
1644
1854
  return `// ${TEAMAI_MARKER} hooks plugin \u2014 generated by teamai, do not edit by hand.
@@ -1720,20 +1930,20 @@ export const TeamaiHooks = async ({ $, directory, worktree }) => {
1720
1930
  async function injectOpencodeHooks(baseDir, scope) {
1721
1931
  const dir = resolveOpencodePluginDir(baseDir, scope);
1722
1932
  await ensureDir(dir);
1723
- const file = path8.join(dir, OPENCODE_HOOK_FILE);
1933
+ const file = path9.join(dir, OPENCODE_HOOK_FILE);
1724
1934
  await writeFile(file, buildPluginSource());
1725
1935
  log.success(`Injected teamai OpenCode hook into ${file}`);
1726
1936
  }
1727
1937
  async function removeOpencodeHooks(baseDir, scope) {
1728
1938
  const dir = resolveOpencodePluginDir(baseDir, scope);
1729
- const file = path8.join(dir, OPENCODE_HOOK_FILE);
1939
+ const file = path9.join(dir, OPENCODE_HOOK_FILE);
1730
1940
  if (await pathExists(file)) {
1731
1941
  await remove(file);
1732
1942
  log.success(`Removed teamai OpenCode hook from ${file}`);
1733
1943
  }
1734
1944
  }
1735
1945
  function assertSafeSlug(slug) {
1736
- if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..") || path8.isAbsolute(slug)) {
1946
+ if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..") || path9.isAbsolute(slug)) {
1737
1947
  throw new Error(`Invalid agent-hook slug: ${slug}`);
1738
1948
  }
1739
1949
  }
@@ -1785,14 +1995,14 @@ async function applyOpencodeAgentHook(def) {
1785
1995
  }
1786
1996
  const dir = resolveOpencodePluginDir(def.baseDir, def.scope);
1787
1997
  await ensureDir(dir);
1788
- const file = path8.join(dir, `teamai-agent-${def.slug}.ts`);
1998
+ const file = path9.join(dir, `teamai-agent-${def.slug}.ts`);
1789
1999
  await writeFile(file, buildAgentHookPluginSource(def.slug, ocEvent, def.command, def.matcher));
1790
2000
  log.success(`Installed OpenCode agent hook [${def.slug}] in ${file}`);
1791
2001
  }
1792
2002
  async function removeOpencodeAgentHook(opts) {
1793
2003
  assertSafeSlug(opts.slug);
1794
2004
  const dir = resolveOpencodePluginDir(opts.baseDir, opts.scope);
1795
- const file = path8.join(dir, `teamai-agent-${opts.slug}.ts`);
2005
+ const file = path9.join(dir, `teamai-agent-${opts.slug}.ts`);
1796
2006
  if (await pathExists(file)) {
1797
2007
  await remove(file);
1798
2008
  log.success(`Removed OpenCode agent hook [${opts.slug}] from ${file}`);
@@ -1828,13 +2038,13 @@ __export(openclaw_hooks_exports, {
1828
2038
  resolveOpenClawHooksDir: () => resolveOpenClawHooksDir,
1829
2039
  resolveOpenclawWorkspaceDir: () => resolveOpenclawWorkspaceDir
1830
2040
  });
1831
- import path9 from "path";
2041
+ import path10 from "path";
1832
2042
  function resolveOpenClawHooksDir(tool) {
1833
2043
  if (tool === "openclaw" && process.env.OPENCLAW_STATE_DIR) {
1834
- return path9.join(process.env.OPENCLAW_STATE_DIR, "hooks");
2044
+ return path10.join(process.env.OPENCLAW_STATE_DIR, "hooks");
1835
2045
  }
1836
2046
  const home = getUserHome();
1837
- return path9.join(home, `.${tool}`, "hooks");
2047
+ return path10.join(home, `.${tool}`, "hooks");
1838
2048
  }
1839
2049
  function buildHookMd(tool) {
1840
2050
  const events = Object.keys(EVENT_MAP);
@@ -1880,21 +2090,21 @@ async function injectOpenClawHooks(workspacePath, tool = "openclaw") {
1880
2090
  log.debug(`openclaw: skip hook injection for ${tool} \u2014 workspace dir not found`);
1881
2091
  return;
1882
2092
  }
1883
- const dir = path9.join(wsDir, "hooks", OPENCLAW_HOOK_DIR);
2093
+ const dir = path10.join(wsDir, "hooks", OPENCLAW_HOOK_DIR);
1884
2094
  await ensureDir(dir);
1885
- await writeFile(path9.join(dir, "HOOK.md"), buildHookMd(tool));
1886
- await writeFile(path9.join(dir, "handler.ts"), buildHandlerTs(tool));
2095
+ await writeFile(path10.join(dir, "HOOK.md"), buildHookMd(tool));
2096
+ await writeFile(path10.join(dir, "handler.ts"), buildHandlerTs(tool));
1887
2097
  log.success(`Injected teamai OpenClaw hook into ${dir}`);
1888
2098
  await enableOpenClawInternalHooks(tool);
1889
2099
  }
1890
2100
  async function enableOpenClawInternalHooks(tool = "openclaw") {
1891
2101
  if (tool !== "openclaw") return;
1892
2102
  const stateDir = process.env.OPENCLAW_STATE_DIR;
1893
- if (!stateDir || !path9.isAbsolute(stateDir)) {
2103
+ if (!stateDir || !path10.isAbsolute(stateDir)) {
1894
2104
  log.debug("openclaw: skip enabling internal hooks \u2014 OPENCLAW_STATE_DIR unset or not absolute");
1895
2105
  return;
1896
2106
  }
1897
- const cfgPath = path9.join(stateDir, "openclaw.json");
2107
+ const cfgPath = path10.join(stateDir, "openclaw.json");
1898
2108
  try {
1899
2109
  const cfg = await readJson(cfgPath) ?? {};
1900
2110
  const hooksVal = cfg.hooks;
@@ -1915,13 +2125,13 @@ async function enableOpenClawInternalHooks(tool = "openclaw") {
1915
2125
  }
1916
2126
  }
1917
2127
  async function removeOpenClawHooks(hooksDir) {
1918
- const dir = path9.join(hooksDir, OPENCLAW_HOOK_DIR);
2128
+ const dir = path10.join(hooksDir, OPENCLAW_HOOK_DIR);
1919
2129
  if (await pathExists(dir)) {
1920
2130
  await remove(dir);
1921
2131
  log.success(`Removed teamai OpenClaw hook from ${dir}`);
1922
2132
  }
1923
2133
  if (process.env.OPENCLAW_STATE_DIR) {
1924
- const altDir = path9.join(process.env.OPENCLAW_STATE_DIR, "hooks", OPENCLAW_HOOK_DIR);
2134
+ const altDir = path10.join(process.env.OPENCLAW_STATE_DIR, "hooks", OPENCLAW_HOOK_DIR);
1925
2135
  if (altDir !== dir && await pathExists(altDir)) {
1926
2136
  await remove(altDir);
1927
2137
  log.success(`Removed teamai OpenClaw hook from ${altDir}`);
@@ -1970,16 +2180,16 @@ async function applyOpenClawAgentHook(def) {
1970
2180
  }
1971
2181
  const tool = def.tool ?? "openclaw";
1972
2182
  const hooksDir = resolveOpenClawHooksDir(tool);
1973
- const dir = path9.join(hooksDir, def.slug);
2183
+ const dir = path10.join(hooksDir, def.slug);
1974
2184
  await ensureDir(dir);
1975
- await writeFile(path9.join(dir, "HOOK.md"), buildAgentHookMd(def.slug, openclawEvent));
1976
- await writeFile(path9.join(dir, "handler.ts"), buildAgentHandlerTs(def.command, def.timeout ?? 10));
2185
+ await writeFile(path10.join(dir, "HOOK.md"), buildAgentHookMd(def.slug, openclawEvent));
2186
+ await writeFile(path10.join(dir, "handler.ts"), buildAgentHandlerTs(def.command, def.timeout ?? 10));
1977
2187
  log.success(`Installed OpenClaw agent hook [${def.slug}] in ${dir}`);
1978
2188
  }
1979
2189
  async function removeOpenClawAgentHook(opts) {
1980
2190
  const tool = opts.tool ?? "openclaw";
1981
2191
  const hooksDir = resolveOpenClawHooksDir(tool);
1982
- const dir = path9.join(hooksDir, opts.slug);
2192
+ const dir = path10.join(hooksDir, opts.slug);
1983
2193
  if (await pathExists(dir)) {
1984
2194
  await remove(dir);
1985
2195
  log.success(`Removed OpenClaw agent hook [${opts.slug}] from ${dir}`);
@@ -1989,8 +2199,8 @@ async function resolveOpenclawWorkspaceDir(workspacePath) {
1989
2199
  const candidates = [];
1990
2200
  if (workspacePath) candidates.push(workspacePath);
1991
2201
  const stateDir = process.env.OPENCLAW_STATE_DIR;
1992
- if (stateDir && path9.isAbsolute(stateDir)) {
1993
- const cfgRaw = await readFileSafe(path9.join(stateDir, "openclaw.json"));
2202
+ if (stateDir && path10.isAbsolute(stateDir)) {
2203
+ const cfgRaw = await readFileSafe(path10.join(stateDir, "openclaw.json"));
1994
2204
  if (cfgRaw) {
1995
2205
  try {
1996
2206
  const cfg = JSON.parse(cfgRaw);
@@ -2001,7 +2211,7 @@ async function resolveOpenclawWorkspaceDir(workspacePath) {
2001
2211
  }
2002
2212
  }
2003
2213
  }
2004
- candidates.push(path9.join(getUserHome(), ".openclaw", "workspace"));
2214
+ candidates.push(path10.join(getUserHome(), ".openclaw", "workspace"));
2005
2215
  for (const candidate of candidates) {
2006
2216
  if (await pathExists(candidate)) {
2007
2217
  log.debug(`openclaw: resolved workspace dir to ${candidate}`);
@@ -2036,14 +2246,14 @@ var hermes_home_exports = {};
2036
2246
  __export(hermes_home_exports, {
2037
2247
  getHermesHome: () => getHermesHome
2038
2248
  });
2039
- import path10 from "path";
2249
+ import path11 from "path";
2040
2250
  import { homedir } from "os";
2041
2251
  function getHermesHome() {
2042
2252
  const fromEnv = process.env.HERMES_HOME;
2043
2253
  if (fromEnv && fromEnv.trim() !== "") {
2044
- return path10.resolve(fromEnv);
2254
+ return path11.resolve(fromEnv);
2045
2255
  }
2046
- return path10.join(homedir(), ".hermes");
2256
+ return path11.join(homedir(), ".hermes");
2047
2257
  }
2048
2258
  var init_hermes_home = __esm({
2049
2259
  "src/hermes-home.ts"() {
@@ -2064,20 +2274,20 @@ __export(hermes_config_exports, {
2064
2274
  upsertHermesHook: () => upsertHermesHook,
2065
2275
  upsertSoulRules: () => upsertSoulRules
2066
2276
  });
2067
- import YAML2 from "yaml";
2068
- import path11 from "path";
2277
+ import YAML3 from "yaml";
2278
+ import path12 from "path";
2069
2279
  function getHermesConfigPath() {
2070
- return path11.join(getHermesHome(), "config.yaml");
2280
+ return path12.join(getHermesHome(), "config.yaml");
2071
2281
  }
2072
2282
  function getHermesSoulPath() {
2073
- return path11.join(getHermesHome(), "SOUL.md");
2283
+ return path12.join(getHermesHome(), "SOUL.md");
2074
2284
  }
2075
2285
  async function readConfigDoc() {
2076
2286
  const content = await readFileSafe(getHermesConfigPath());
2077
- if (!content || content.trim() === "") return new YAML2.Document({});
2078
- const doc = YAML2.parseDocument(content);
2079
- if (!doc.contents || !YAML2.isMap(doc.contents)) {
2080
- return new YAML2.Document({});
2287
+ if (!content || content.trim() === "") return new YAML3.Document({});
2288
+ const doc = YAML3.parseDocument(content);
2289
+ if (!doc.contents || !YAML3.isMap(doc.contents)) {
2290
+ return new YAML3.Document({});
2081
2291
  }
2082
2292
  return doc;
2083
2293
  }
@@ -2150,12 +2360,12 @@ async function removeSoulRules() {
2150
2360
  await upsertSoulRules("");
2151
2361
  }
2152
2362
  function getHermesAllowlistPath() {
2153
- return path11.join(getHermesHome(), "shell-hooks-allowlist.json");
2363
+ return path12.join(getHermesHome(), "shell-hooks-allowlist.json");
2154
2364
  }
2155
2365
  async function upsertHermesHook(event, entry) {
2156
2366
  const doc = await readConfigDoc();
2157
2367
  const rawSeq = doc.getIn(["hooks", event]);
2158
- const currentJs = YAML2.isSeq(rawSeq) ? rawSeq.toJSON() : rawSeq;
2368
+ const currentJs = YAML3.isSeq(rawSeq) ? rawSeq.toJSON() : rawSeq;
2159
2369
  const arr = Array.isArray(currentJs) ? currentJs : [];
2160
2370
  const untouched = arr.filter((e) => e && typeof e === "object" && e.command !== entry.command);
2161
2371
  const cleanEntry = { command: entry.command };
@@ -2169,7 +2379,7 @@ async function upsertHermesHook(event, entry) {
2169
2379
  async function removeHermesHookByCommand(command) {
2170
2380
  const doc = await readConfigDoc();
2171
2381
  const rawHooks = doc.getIn(["hooks"]);
2172
- const hooks = YAML2.isMap(rawHooks) ? rawHooks.toJSON() : null;
2382
+ const hooks = YAML3.isMap(rawHooks) ? rawHooks.toJSON() : null;
2173
2383
  if (!hooks || typeof hooks !== "object") return;
2174
2384
  let changed = false;
2175
2385
  for (const event of Object.keys(hooks)) {
@@ -2186,7 +2396,7 @@ async function removeHermesHookByCommand(command) {
2186
2396
  }
2187
2397
  if (!changed) return;
2188
2398
  const remainingRaw = doc.getIn(["hooks"]);
2189
- const remaining = YAML2.isMap(remainingRaw) ? remainingRaw.toJSON() : null;
2399
+ const remaining = YAML3.isMap(remainingRaw) ? remainingRaw.toJSON() : null;
2190
2400
  if (!remaining || Object.keys(remaining).length === 0) {
2191
2401
  doc.deleteIn(["hooks"]);
2192
2402
  }
@@ -2231,10 +2441,10 @@ __export(hermes_hooks_exports, {
2231
2441
  removeHermesAgentHook: () => removeHermesAgentHook,
2232
2442
  removeHermesHooks: () => removeHermesHooks
2233
2443
  });
2234
- import path12 from "path";
2444
+ import path13 from "path";
2235
2445
  import { chmod } from "fs/promises";
2236
2446
  function getReportScriptPath() {
2237
- return path12.join(getHermesHome(), "hooks", "teamai-status-report.sh");
2447
+ return path13.join(getHermesHome(), "hooks", "teamai-status-report.sh");
2238
2448
  }
2239
2449
  function buildReportScript() {
2240
2450
  return [
@@ -2246,7 +2456,7 @@ function buildReportScript() {
2246
2456
  }
2247
2457
  async function injectHermesHooks() {
2248
2458
  const scriptPath = getReportScriptPath();
2249
- await ensureDir(path12.dirname(scriptPath));
2459
+ await ensureDir(path13.dirname(scriptPath));
2250
2460
  await writeFile(scriptPath, buildReportScript());
2251
2461
  try {
2252
2462
  await chmod(scriptPath, 493);
@@ -2321,7 +2531,7 @@ __export(hooks_exports, {
2321
2531
  removeHooks: () => removeHooks,
2322
2532
  sweepLegacyProjectHooks: () => sweepLegacyProjectHooks
2323
2533
  });
2324
- import path13 from "path";
2534
+ import path14 from "path";
2325
2535
  import { realpathSync } from "fs";
2326
2536
  function detectFormat(tool) {
2327
2537
  if (CODEX_TOOLS.has(tool)) return "codex";
@@ -2354,7 +2564,7 @@ function canonicalProjectRoot(projectRoot) {
2354
2564
  try {
2355
2565
  return realpathSync.native(projectRoot);
2356
2566
  } catch {
2357
- return path13.resolve(projectRoot);
2567
+ return path14.resolve(projectRoot);
2358
2568
  }
2359
2569
  }
2360
2570
  function gateTeamHookCommand(command, projectRoot) {
@@ -2414,16 +2624,31 @@ function toCodexEntry(def) {
2414
2624
  return entry;
2415
2625
  }
2416
2626
  function toZcodeEntry(def) {
2417
- const entry = {
2627
+ const ZCODE_TIMEOUT_MS = {
2628
+ SessionStart: 18e4,
2629
+ Stop: 6e4,
2630
+ PostToolUse: 3e4,
2631
+ UserPromptSubmit: 6e4
2632
+ };
2633
+ const timeoutMs = def.timeout !== void 0 ? def.timeout * 1e3 : ZCODE_TIMEOUT_MS[def.event] ?? 6e4;
2634
+ const entry = process.platform === "win32" ? {
2635
+ // Windows must NOT spawn bare `bash`: CreateProcess resolves it to
2636
+ // System32's WSL launcher before any PATH directory, and the WSL side
2637
+ // has a different $HOME (no ~/.teamai state) and often no Node ≥ 20.
2638
+ // cmd.exe is always present in System32 and resolves teamai from the
2639
+ // Windows PATH (the npm shim is a .cmd, so a shell is required).
2640
+ type: "process",
2641
+ command: "cmd",
2642
+ args: ["/c", def.command],
2643
+ timeoutMs
2644
+ } : {
2418
2645
  type: "process",
2419
2646
  command: "bash",
2420
- // Stored verbatim: the shell payload must equal `def.command` exactly so
2421
- // managed-entry detection and the managed-hooks manifest share one command
2422
- // representation (the same invariant the Codex format keeps). teamai
2423
- // hook-dispatch is silent and failure-tolerant on its success paths, so no
2424
- // shell redirection is layered on top of the payload.
2647
+ // Stored verbatim: the shell payload must equal `def.command` exactly
2648
+ // so managed-entry detection and the managed-hooks manifest share one
2649
+ // command representation (the same invariant the Codex format keeps).
2425
2650
  args: ["-lc", def.command],
2426
- ...def.timeout !== void 0 ? { timeoutMs: def.timeout * 1e3 } : {}
2651
+ timeoutMs
2427
2652
  };
2428
2653
  const group = { hooks: [entry] };
2429
2654
  if (def.matcher && def.matcher !== "*") group.matcher = def.matcher;
@@ -2431,7 +2656,7 @@ function toZcodeEntry(def) {
2431
2656
  }
2432
2657
  function zcodeEntryCommand(entry) {
2433
2658
  const hook = entry.hooks?.[0];
2434
- if (hook?.command === "bash" && hook.args?.[0] === "-lc") return hook.args[1] ?? "";
2659
+ if (Array.isArray(hook?.args) && hook.args.length > 1) return hook.args[1] ?? "";
2435
2660
  return hook?.command ?? "";
2436
2661
  }
2437
2662
  function desiredEventOrder(defs, mapEvent) {
@@ -2465,7 +2690,7 @@ async function reconcileClaudeFormat(settingsPath, tool, teamDefs, opts, teamAct
2465
2690
  return true;
2466
2691
  };
2467
2692
  const expanded = expandHome(settingsPath);
2468
- await ensureDir(path13.dirname(expanded));
2693
+ await ensureDir(path14.dirname(expanded));
2469
2694
  const settings = await readJson(expanded) ?? {};
2470
2695
  if (!settings.hooks) settings.hooks = {};
2471
2696
  let changed = false;
@@ -2497,7 +2722,7 @@ async function reconcileClaudeFormat(settingsPath, tool, teamDefs, opts, teamAct
2497
2722
  }
2498
2723
  async function reconcileCursorFormat(hooksPath, tool, teamDefs, opts, priorTeamCommands) {
2499
2724
  const expanded = expandHome(hooksPath);
2500
- await ensureDir(path13.dirname(expanded));
2725
+ await ensureDir(path14.dirname(expanded));
2501
2726
  const hooksJson = await readJson(expanded) ?? { version: 1, hooks: {} };
2502
2727
  if (!hooksJson.version) hooksJson.version = 1;
2503
2728
  if (!hooksJson.hooks) hooksJson.hooks = {};
@@ -2545,7 +2770,7 @@ async function reconcileCursorFormat(hooksPath, tool, teamDefs, opts, priorTeamC
2545
2770
  }
2546
2771
  async function reconcileCodexFormat(hooksPath, tool, teamDefs, opts, priorTeamCommands) {
2547
2772
  const expanded = expandHome(hooksPath);
2548
- await ensureDir(path13.dirname(expanded));
2773
+ await ensureDir(path14.dirname(expanded));
2549
2774
  const hooksJson = await readJson(expanded) ?? {};
2550
2775
  if (!hooksJson.hooks) hooksJson.hooks = {};
2551
2776
  const isManaged = (entry) => {
@@ -2575,10 +2800,15 @@ async function reconcileCodexFormat(hooksPath, tool, teamDefs, opts, priorTeamCo
2575
2800
  }
2576
2801
  async function reconcileZcodeFormat(settingsPath, tool, teamDefs, opts, priorTeamCommands) {
2577
2802
  const expanded = expandHome(settingsPath);
2578
- await ensureDir(path13.dirname(expanded));
2803
+ await ensureDir(path14.dirname(expanded));
2579
2804
  const cfg = await readJson(expanded) ?? {};
2580
2805
  if (!cfg.hooks) cfg.hooks = {};
2581
2806
  let changed = false;
2807
+ const unknownHookKeys = Object.keys(cfg.hooks).filter((k) => k !== "enabled" && k !== "events");
2808
+ if (unknownHookKeys.length > 0) {
2809
+ for (const k of unknownHookKeys) delete cfg.hooks[k];
2810
+ changed = true;
2811
+ }
2582
2812
  if (!opts.removeAll && cfg.hooks.enabled !== true) {
2583
2813
  cfg.hooks.enabled = true;
2584
2814
  changed = true;
@@ -2628,7 +2858,7 @@ function isAgentClaudeEntry(entry, slug) {
2628
2858
  async function applyAgentHook(settingsPath, tool, def) {
2629
2859
  const format = detectFormat(tool);
2630
2860
  const expanded = expandHome(settingsPath);
2631
- await ensureDir(path13.dirname(expanded));
2861
+ await ensureDir(path14.dirname(expanded));
2632
2862
  const hookDef = {
2633
2863
  source: "team",
2634
2864
  key: def.slug,
@@ -2854,15 +3084,15 @@ async function hasTeamaiHooks(settingsPath, tool, manifestPath) {
2854
3084
  async function reconcileOpencodePlugin(baseDir, removeAll = false, installedBaseDir) {
2855
3085
  const home = getUserHome();
2856
3086
  const { injectOpencodeHooks: injectOpencodeHooks2, removeOpencodeHooks: removeOpencodeHooks2 } = await Promise.resolve().then(() => (init_opencode_hooks(), opencode_hooks_exports));
2857
- if (path13.resolve(baseDir) !== path13.resolve(home)) {
3087
+ if (path14.resolve(baseDir) !== path14.resolve(home)) {
2858
3088
  await removeOpencodeHooks2(baseDir, "project");
2859
3089
  }
2860
3090
  if (removeAll) {
2861
3091
  await removeOpencodeHooks2(home, "user");
2862
3092
  return;
2863
3093
  }
2864
- const homeInstalled = await pathExists(path13.join(home, ".config", "opencode"));
2865
- const projectInstalled = installedBaseDir ? await pathExists(path13.join(installedBaseDir, ".opencode")) : false;
3094
+ const homeInstalled = await pathExists(path14.join(home, ".config", "opencode"));
3095
+ const projectInstalled = installedBaseDir ? await pathExists(path14.join(installedBaseDir, ".opencode")) : false;
2866
3096
  if (homeInstalled || projectInstalled) {
2867
3097
  await injectOpencodeHooks2(home, "user");
2868
3098
  }
@@ -2876,9 +3106,9 @@ async function injectHooksToAllTools(toolPaths, baseDir, filterAgents2) {
2876
3106
  if (filterAgents2 && !filterAgents2.includes(tool)) continue;
2877
3107
  if (skipped.has(tool)) continue;
2878
3108
  if (paths.settings) {
2879
- const toolRoot = path13.join(resolvedBaseDir, paths.settings.split("/")[0]);
3109
+ const toolRoot = path14.join(resolvedBaseDir, paths.settings.split("/")[0]);
2880
3110
  if (!await pathExists(toolRoot)) continue;
2881
- const settingsPath = path13.join(resolvedBaseDir, paths.settings);
3111
+ const settingsPath = path14.join(resolvedBaseDir, paths.settings);
2882
3112
  try {
2883
3113
  await injectHooks(settingsPath, tool);
2884
3114
  } catch (e) {
@@ -2941,10 +3171,10 @@ async function reconcileHooksToAllTools(toolPaths, baseDir, teamDefs, manifestPa
2941
3171
  continue;
2942
3172
  }
2943
3173
  if (!paths.settings) continue;
2944
- const toolRoot = path13.join(baseDir, paths.settings.split("/")[0]);
2945
- const installedRoot = opts.installedBaseDir ? path13.join(opts.installedBaseDir, paths.settings.split("/")[0]) : toolRoot;
3174
+ const toolRoot = path14.join(baseDir, paths.settings.split("/")[0]);
3175
+ const installedRoot = opts.installedBaseDir ? path14.join(opts.installedBaseDir, paths.settings.split("/")[0]) : toolRoot;
2946
3176
  if (!await pathExists(toolRoot) && !await pathExists(installedRoot)) continue;
2947
- const settingsPath = path13.join(baseDir, paths.settings);
3177
+ const settingsPath = path14.join(baseDir, paths.settings);
2948
3178
  try {
2949
3179
  await reconcileHooks(settingsPath, tool, teamDefs, {
2950
3180
  manifestPath,
@@ -2960,7 +3190,7 @@ async function reconcileHooksToAllTools(toolPaths, baseDir, teamDefs, manifestPa
2960
3190
  async function hasInstalledCodexTrustGatedTool(toolPaths, baseDir) {
2961
3191
  for (const [tool, paths] of Object.entries(toolPaths)) {
2962
3192
  if (!isCodexTrustGatedTool(tool) || !paths.settings) continue;
2963
- const toolRoot = path13.join(baseDir, paths.settings.split("/")[0]);
3193
+ const toolRoot = path14.join(baseDir, paths.settings.split("/")[0]);
2964
3194
  if (await pathExists(toolRoot)) return true;
2965
3195
  }
2966
3196
  return false;
@@ -2982,7 +3212,11 @@ async function sweepLegacyProjectHooks(toolPaths, localConfig) {
2982
3212
  }
2983
3213
  }
2984
3214
  async function reconcileTeamHooksForConfig(teamConfig, localConfig, opts = {}) {
2985
- const { defs: teamDefs, builtin } = opts.removeAll ? { defs: [], builtin: void 0 } : await resolveTeamHooks(teamConfig, localConfig.repo.localPath, { auto: opts.auto, silent: opts.silent });
3215
+ const { defs: teamDefs, builtin } = opts.removeAll ? { defs: [], builtin: void 0 } : await resolveTeamHooks(teamConfig, localConfig.repo.localPath, {
3216
+ auto: opts.auto,
3217
+ silent: opts.silent,
3218
+ activeRoles: activeRoleIds(localConfig)
3219
+ });
2986
3220
  const { baseDir, manifestPath } = resolveHookScope(localConfig);
2987
3221
  let filterAgents2 = opts.filterAgents ?? localConfig.enabledAgents;
2988
3222
  const disabled = localConfig.disabledAgents;
@@ -3008,6 +3242,7 @@ var init_hooks2 = __esm({
3008
3242
  init_logger();
3009
3243
  init_types();
3010
3244
  init_types();
3245
+ init_roles();
3011
3246
  init_builtin_hooks();
3012
3247
  init_hooks();
3013
3248
  init_home();
@@ -3248,7 +3483,7 @@ __export(builtin_rules_exports, {
3248
3483
  LEGACY_RULE_NAMES: () => LEGACY_RULE_NAMES,
3249
3484
  deployBuiltinRules: () => deployBuiltinRules
3250
3485
  });
3251
- import path14 from "path";
3486
+ import path15 from "path";
3252
3487
  import fs4 from "fs/promises";
3253
3488
  async function deployBuiltinRules(teamConfig, localConfig, options) {
3254
3489
  const baseDir = localConfig ? resolveBaseDir(localConfig) : getUserHome();
@@ -3263,19 +3498,19 @@ async function deployBuiltinRules(teamConfig, localConfig, options) {
3263
3498
  continue;
3264
3499
  }
3265
3500
  if (localConfig && isAgentExcluded(localConfig, tool)) continue;
3266
- const rulesDir = path14.join(baseDir, toolPath.rules);
3501
+ const rulesDir = path15.join(baseDir, toolPath.rules);
3267
3502
  if (!await pathExists(rulesDir)) continue;
3268
3503
  try {
3269
3504
  await ensureDir(rulesDir);
3270
3505
  const ext = ruleFileExtensionForTool(tool);
3271
3506
  for (const rule of builtinRules) {
3272
- const destFile = path14.join(rulesDir, `${rule.name}${ext}`);
3507
+ const destFile = path15.join(rulesDir, `${rule.name}${ext}`);
3273
3508
  const content = usesCursorMdcRules(tool) ? teamRuleToCursorMdc(rule.content) : rule.content;
3274
3509
  await writeFile(destFile, content);
3275
3510
  log.debug(`Deployed built-in rule ${rule.name} \u2192 ${tool}`);
3276
3511
  if (ext !== ".md") {
3277
3512
  try {
3278
- await fs4.unlink(path14.join(rulesDir, `${rule.name}.md`));
3513
+ await fs4.unlink(path15.join(rulesDir, `${rule.name}.md`));
3279
3514
  log.debug(`Removed legacy .md built-in rule ${rule.name} from ${tool}`);
3280
3515
  } catch {
3281
3516
  }
@@ -3283,7 +3518,7 @@ async function deployBuiltinRules(teamConfig, localConfig, options) {
3283
3518
  }
3284
3519
  for (const legacyName of LEGACY_RULE_NAMES) {
3285
3520
  for (const legacyExt of /* @__PURE__ */ new Set([ext, ".md"])) {
3286
- const legacyFile = path14.join(rulesDir, `${legacyName}${legacyExt}`);
3521
+ const legacyFile = path15.join(rulesDir, `${legacyName}${legacyExt}`);
3287
3522
  try {
3288
3523
  await fs4.unlink(legacyFile);
3289
3524
  log.debug(`Removed legacy built-in rule ${legacyName} from ${tool}`);
@@ -3370,7 +3605,7 @@ teamai-recall subagent \u7684\u8FD4\u56DE\u91CC\u5DF2\u5217\u51FA\u672C\u6B21\u6
3370
3605
  });
3371
3606
 
3372
3607
  // src/utils/pre-push-sync.ts
3373
- import path15 from "path";
3608
+ import path16 from "path";
3374
3609
  async function syncTeamUpdatesToLocal(teamConfig, localConfig, lastPullRev) {
3375
3610
  if (!lastPullRev) {
3376
3611
  log.debug("No lastPullRev \u2014 skipping pre-push sync");
@@ -3382,12 +3617,12 @@ async function syncTeamUpdatesToLocal(teamConfig, localConfig, lastPullRev) {
3382
3617
  await syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, lastPullRev);
3383
3618
  }
3384
3619
  async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, lastPullRev) {
3385
- const teamRulesDir = path15.join(repoPath, "rules");
3620
+ const teamRulesDir = path16.join(repoPath, "rules");
3386
3621
  if (!await pathExists(teamRulesDir)) return;
3387
3622
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
3388
3623
  if (!toolPath.rules) continue;
3389
3624
  if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
3390
- const rulesDir = path15.join(baseDir, toolPath.rules);
3625
+ const rulesDir = path16.join(baseDir, toolPath.rules);
3391
3626
  if (!await pathExists(rulesDir)) continue;
3392
3627
  const ext = ruleFileExtensionForTool(tool);
3393
3628
  const isMdcTool = usesCursorMdcRules(tool);
@@ -3396,9 +3631,9 @@ async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, last
3396
3631
  if (!file.endsWith(ext)) continue;
3397
3632
  const name = file.slice(0, -ext.length);
3398
3633
  if (EXCLUDED_RULE_NAMES.has(name)) continue;
3399
- const localFilePath = path15.join(rulesDir, file);
3634
+ const localFilePath = path16.join(rulesDir, file);
3400
3635
  const teamRelPath = `rules/${name}.md`;
3401
- const teamFilePath = path15.join(teamRulesDir, `${name}.md`);
3636
+ const teamFilePath = path16.join(teamRulesDir, `${name}.md`);
3402
3637
  if (!await pathExists(teamFilePath)) continue;
3403
3638
  if (isMdcTool) {
3404
3639
  const localRaw = await readFileSafe(localFilePath);
@@ -3424,19 +3659,19 @@ async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, last
3424
3659
  }
3425
3660
  }
3426
3661
  async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, lastPullRev) {
3427
- const teamSkillsDir = path15.join(repoPath, "skills");
3662
+ const teamSkillsDir = path16.join(repoPath, "skills");
3428
3663
  if (!await pathExists(teamSkillsDir)) return;
3429
3664
  const teamSkillPaths = /* @__PURE__ */ new Map();
3430
3665
  const topDirs = await listDirs(teamSkillsDir);
3431
3666
  for (const dir of topDirs) {
3432
- const dirPath = path15.join(teamSkillsDir, dir);
3433
- if (await pathExists(path15.join(dirPath, "SKILL.md"))) {
3667
+ const dirPath = path16.join(teamSkillsDir, dir);
3668
+ if (await pathExists(path16.join(dirPath, "SKILL.md"))) {
3434
3669
  teamSkillPaths.set(dir, dirPath);
3435
3670
  } else {
3436
3671
  const subDirs = await listDirs(dirPath);
3437
3672
  for (const subDir of subDirs) {
3438
3673
  if (!teamSkillPaths.has(subDir)) {
3439
- teamSkillPaths.set(subDir, path15.join(dirPath, subDir));
3674
+ teamSkillPaths.set(subDir, path16.join(dirPath, subDir));
3440
3675
  }
3441
3676
  }
3442
3677
  }
@@ -3445,12 +3680,12 @@ async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, las
3445
3680
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
3446
3681
  if (!toolPath.skills) continue;
3447
3682
  if (!await ResourceHandler.isToolInstalled(toolPath.skills, baseDir)) continue;
3448
- const skillsDir = path15.join(baseDir, toolPath.skills);
3683
+ const skillsDir = path16.join(baseDir, toolPath.skills);
3449
3684
  if (!await pathExists(skillsDir)) continue;
3450
3685
  const localSkillNames = await listDirs(skillsDir);
3451
3686
  for (const skillName of localSkillNames) {
3452
3687
  if (!teamSkillPaths.has(skillName)) continue;
3453
- const localSkillDir = path15.join(skillsDir, skillName);
3688
+ const localSkillDir = path16.join(skillsDir, skillName);
3454
3689
  const teamSkillDir = teamSkillPaths.get(skillName);
3455
3690
  if (await dirTeamSubsetEqual(localSkillDir, teamSkillDir, [CONTRIBUTORS_FILE3])) continue;
3456
3691
  const teamFiles = await listFilesRecursive(teamSkillDir);
@@ -3458,10 +3693,10 @@ async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, las
3458
3693
  let anyDiffers = false;
3459
3694
  for (const file of teamFiles) {
3460
3695
  if (file === CONTRIBUTORS_FILE3) continue;
3461
- const localFile = path15.join(localSkillDir, file);
3462
- const teamFile = path15.join(teamSkillDir, file);
3696
+ const localFile = path16.join(localSkillDir, file);
3697
+ const teamFile = path16.join(teamSkillDir, file);
3463
3698
  if (!await pathExists(localFile)) {
3464
- const relFromRepo2 = path15.relative(repoPath, teamFile);
3699
+ const relFromRepo2 = path16.relative(repoPath, teamFile);
3465
3700
  const oldContent2 = await getFileContentAtRev(repoPath, lastPullRev, relFromRepo2);
3466
3701
  if (oldContent2 === null) {
3467
3702
  anyDiffers = true;
@@ -3472,7 +3707,7 @@ async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, las
3472
3707
  }
3473
3708
  if (await fileContentEqual(localFile, teamFile)) continue;
3474
3709
  anyDiffers = true;
3475
- const relFromRepo = path15.relative(repoPath, teamFile);
3710
+ const relFromRepo = path16.relative(repoPath, teamFile);
3476
3711
  const oldContent = await getFileContentAtRev(repoPath, lastPullRev, relFromRepo);
3477
3712
  if (oldContent === null) {
3478
3713
  allMatchOld = false;
@@ -3604,12 +3839,12 @@ var init_rest_auth = __esm({
3604
3839
  import { execSync, spawnSync } from "child_process";
3605
3840
  import fs5 from "fs";
3606
3841
  import os2 from "os";
3607
- import path16 from "path";
3842
+ import path17 from "path";
3608
3843
  function gfInstallDir() {
3609
- return path16.join(getTeamaiHomeDir(), "gf");
3844
+ return path17.join(getTeamaiHomeDir(), "gf");
3610
3845
  }
3611
3846
  function gfBinPath() {
3612
- return path16.join(gfInstallDir(), "gf", "bin", "gf");
3847
+ return path17.join(gfInstallDir(), "gf", "bin", "gf");
3613
3848
  }
3614
3849
  function shellQuote2(s) {
3615
3850
  return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -3747,7 +3982,7 @@ function ensureAuthenticated() {
3747
3982
  }
3748
3983
  function gfGetOAuthToken() {
3749
3984
  try {
3750
- const netrcPath2 = path16.join(os2.homedir(), ".netrc");
3985
+ const netrcPath2 = path17.join(os2.homedir(), ".netrc");
3751
3986
  if (!fs5.existsSync(netrcPath2)) return null;
3752
3987
  const content = fs5.readFileSync(netrcPath2, "utf-8");
3753
3988
  const match = content.match(
@@ -5599,7 +5834,7 @@ var init_repo_url4 = __esm({
5599
5834
  import { spawnSync as spawnSync5 } from "child_process";
5600
5835
  import fs6 from "fs";
5601
5836
  import os3 from "os";
5602
- import path17 from "path";
5837
+ import path18 from "path";
5603
5838
  function gitcodeApiBase() {
5604
5839
  return GITCODE_API_BASE;
5605
5840
  }
@@ -5627,7 +5862,7 @@ function authHeaders3(token) {
5627
5862
  };
5628
5863
  }
5629
5864
  function netrcPath() {
5630
- return path17.join(os3.homedir(), ".netrc");
5865
+ return path18.join(os3.homedir(), ".netrc");
5631
5866
  }
5632
5867
  function readNetrcToken() {
5633
5868
  try {
@@ -6113,12 +6348,13 @@ function buildRepoInfo5(owner, repo, remoteUrl) {
6113
6348
  projectId: encodeURIComponent(`${owner}/${repo}`)
6114
6349
  };
6115
6350
  }
6116
- function parseGenericGitRepoInput(input) {
6351
+ function parseGenericGitRepoInputWithOptions(input, options = {}) {
6117
6352
  const trimmed = input.trim();
6118
- if (/^http:\/\//i.test(trimmed)) {
6353
+ const allowExistingInsecureOrigin = options.allowExistingInsecureOrigin === true;
6354
+ if (/^http:\/\//i.test(trimmed) && !allowExistingInsecureOrigin) {
6119
6355
  throw invalidRepoUrl("plain HTTP is not supported; use HTTPS or SSH");
6120
6356
  }
6121
- if (/^https:\/\//i.test(trimmed) || /^ssh:\/\//i.test(trimmed)) {
6357
+ if (/^https?:\/\//i.test(trimmed) || /^ssh:\/\//i.test(trimmed)) {
6122
6358
  let parsed;
6123
6359
  try {
6124
6360
  parsed = new URL(trimmed);
@@ -6126,7 +6362,7 @@ function parseGenericGitRepoInput(input) {
6126
6362
  throw invalidRepoUrl();
6127
6363
  }
6128
6364
  const httpCredentials = /^https?:$/i.test(parsed.protocol) && (parsed.username || parsed.password);
6129
- if (!parsed.hostname || parsed.password || httpCredentials) {
6365
+ if (!parsed.hostname || !allowExistingInsecureOrigin && (parsed.password || httpCredentials)) {
6130
6366
  throw new Error(
6131
6367
  "Invalid Git repo URL. Do not embed credentials in the URL; configure a Git credential helper or SSH key instead."
6132
6368
  );
@@ -6135,7 +6371,7 @@ function parseGenericGitRepoInput(input) {
6135
6371
  throw invalidRepoUrl("query strings and fragments are not supported");
6136
6372
  }
6137
6373
  const { owner, repo, fullPath } = parsePath(parsed.pathname);
6138
- const auth = parsed.username ? `${parsed.username}@` : "";
6374
+ const auth = /^ssh:$/i.test(parsed.protocol) && parsed.username ? `${parsed.username}@` : "";
6139
6375
  const remoteUrl = `${parsed.protocol}//${auth}${parsed.host}/${fullPath}.git`;
6140
6376
  return buildRepoInfo5(owner, repo, remoteUrl);
6141
6377
  }
@@ -6152,6 +6388,12 @@ function parseGenericGitRepoInput(input) {
6152
6388
  }
6153
6389
  throw invalidRepoUrl();
6154
6390
  }
6391
+ function parseGenericGitRepoInput(input) {
6392
+ return parseGenericGitRepoInputWithOptions(input);
6393
+ }
6394
+ function parseGenericGitExistingRemote(input) {
6395
+ return parseGenericGitRepoInputWithOptions(input, { allowExistingInsecureOrigin: true });
6396
+ }
6155
6397
  var SUPPORTED_URL_HINT;
6156
6398
  var init_repo_url5 = __esm({
6157
6399
  "src/providers/git/repo-url.ts"() {
@@ -6527,17 +6769,17 @@ __export(builtin_skills_exports, {
6527
6769
  deployBuiltinSkills: () => deployBuiltinSkills
6528
6770
  });
6529
6771
  import fs7 from "fs";
6530
- import path18 from "path";
6772
+ import path19 from "path";
6531
6773
  import { fileURLToPath as fileURLToPath2 } from "url";
6532
6774
  import fse2 from "fs-extra";
6533
6775
  function getBuiltinSkillsDir() {
6534
- const distDir = path18.dirname(fileURLToPath2(import.meta.url));
6535
- return path18.join(distDir, "..", "skills");
6776
+ const distDir = path19.dirname(fileURLToPath2(import.meta.url));
6777
+ return path19.join(distDir, "..", "skills");
6536
6778
  }
6537
6779
  async function copyBuiltinSkillDir(srcDir, destDir) {
6538
6780
  await fse2.copy(srcDir, destDir, {
6539
6781
  overwrite: true,
6540
- filter: (srcPath) => !path18.basename(srcPath).startsWith(".")
6782
+ filter: (srcPath) => !path19.basename(srcPath).startsWith(".")
6541
6783
  });
6542
6784
  }
6543
6785
  async function deployBuiltinSkills(teamConfig, localConfig, options) {
@@ -6559,7 +6801,7 @@ async function deployBuiltinSkills(teamConfig, localConfig, options) {
6559
6801
  const skillNames = [];
6560
6802
  for (const entry of entries) {
6561
6803
  if (options?.skipRecall && RECALL_DEPENDENT_SKILLS.has(entry)) continue;
6562
- const skillMd = path18.join(builtinDir, entry, "SKILL.md");
6804
+ const skillMd = path19.join(builtinDir, entry, "SKILL.md");
6563
6805
  if (await pathExists(skillMd)) {
6564
6806
  skillNames.push(entry);
6565
6807
  }
@@ -6575,7 +6817,7 @@ async function deployBuiltinSkills(teamConfig, localConfig, options) {
6575
6817
  }
6576
6818
  if (localConfig && isAgentExcluded(localConfig, tool)) continue;
6577
6819
  for (const skillName of skillNames) {
6578
- const srcDir = path18.join(builtinDir, skillName);
6820
+ const srcDir = path19.join(builtinDir, skillName);
6579
6821
  const destDir = await resolveSkillDestination(tool, toolPath.skills, baseDir, skillName, srcDir);
6580
6822
  try {
6581
6823
  await copyBuiltinSkillDir(srcDir, destDir);
@@ -6603,142 +6845,6 @@ var init_builtin_skills = __esm({
6603
6845
  }
6604
6846
  });
6605
6847
 
6606
- // src/roles.ts
6607
- var roles_exports = {};
6608
- __export(roles_exports, {
6609
- describeRoles: () => describeRoles,
6610
- findRole: () => findRole,
6611
- listRoleIds: () => listRoleIds,
6612
- loadRolesManifest: () => loadRolesManifest,
6613
- resolveRoleResourceNamespaces: () => resolveRoleResourceNamespaces,
6614
- saveRolesManifest: () => saveRolesManifest
6615
- });
6616
- import path19 from "path";
6617
- import YAML3 from "yaml";
6618
- import { z as z3 } from "zod";
6619
- function validateManifestShape(raw) {
6620
- if (!raw || typeof raw !== "object") {
6621
- throw new Error("Invalid roles manifest: expected an object");
6622
- }
6623
- const candidate = raw;
6624
- const roles = candidate.roles;
6625
- if (!Array.isArray(roles) || roles.length === 0) {
6626
- throw new Error("Invalid roles manifest: roles must be a non-empty array");
6627
- }
6628
- for (const role of roles) {
6629
- if (!role || typeof role !== "object") {
6630
- throw new Error("Invalid roles manifest: every role must be an object");
6631
- }
6632
- const resources = role.resources;
6633
- if (!resources || typeof resources !== "object" || Array.isArray(resources)) {
6634
- throw new Error(`Invalid roles manifest: role ${role.id ?? "<unknown>"} is missing resources`);
6635
- }
6636
- const ALLOWED_RESOURCE_KEYS = /* @__PURE__ */ new Set([...ROLE_RESOURCE_TYPES, "learnings"]);
6637
- for (const key of Object.keys(resources)) {
6638
- if (!ALLOWED_RESOURCE_KEYS.has(key)) {
6639
- throw new Error(`Invalid roles manifest: unknown resource type "${key}"`);
6640
- }
6641
- }
6642
- }
6643
- const manifest = RolesManifestSchema.parse(raw);
6644
- const ids = /* @__PURE__ */ new Set();
6645
- for (const role of manifest.roles) {
6646
- if (ids.has(role.id)) {
6647
- throw new Error(`Invalid roles manifest: duplicate role id "${role.id}"`);
6648
- }
6649
- ids.add(role.id);
6650
- }
6651
- return manifest;
6652
- }
6653
- async function loadRolesManifest(repoPath) {
6654
- const manifestPath = path19.join(repoPath, "manifest", "roles.yaml");
6655
- const content = await readFileSafe(manifestPath);
6656
- if (!content) {
6657
- throw new Error(`Roles manifest not found: ${manifestPath}`);
6658
- }
6659
- let raw;
6660
- try {
6661
- raw = YAML3.parse(content);
6662
- } catch (error) {
6663
- throw new Error(`Invalid roles manifest YAML: ${error.message}`);
6664
- }
6665
- return validateManifestShape(raw);
6666
- }
6667
- async function saveRolesManifest(repoPath, manifest) {
6668
- validateManifestShape(manifest);
6669
- const manifestDir = path19.join(repoPath, "manifest");
6670
- const manifestPath = path19.join(manifestDir, "roles.yaml");
6671
- await ensureDir(manifestDir);
6672
- await writeFile(manifestPath, YAML3.stringify(manifest));
6673
- }
6674
- function findRole(manifest, roleId) {
6675
- return manifest.roles.find((candidate) => candidate.id === roleId);
6676
- }
6677
- function listRoleIds(manifest) {
6678
- return manifest.roles.map((role) => role.id);
6679
- }
6680
- function describeRoles(roles) {
6681
- return roles.map((role) => role.description ? `${role.id}: ${role.description}` : `${role.id}`);
6682
- }
6683
- function getRoleOrThrow(manifest, roleId) {
6684
- const role = manifest.roles.find((candidate) => candidate.id === roleId);
6685
- if (!role) {
6686
- throw new Error(`Unknown role "${roleId}". Valid roles: ${listRoleIds(manifest).join(", ")}`);
6687
- }
6688
- return role;
6689
- }
6690
- function resolveRoleResourceNamespaces(input) {
6691
- const resolvedRoles = [
6692
- getRoleOrThrow(input.manifest, input.primaryRole),
6693
- ...input.additionalRoles.map((roleId) => getRoleOrThrow(input.manifest, roleId))
6694
- ];
6695
- const namespaces = {
6696
- knowledge: [],
6697
- skills: [],
6698
- // Roles never contribute learnings namespaces; only projects do. Kept empty
6699
- // so the shape matches project resolution for a clean union at the call site.
6700
- learnings: []
6701
- };
6702
- for (const type of ROLE_RESOURCE_TYPES) {
6703
- const seen = /* @__PURE__ */ new Set();
6704
- for (const role of resolvedRoles) {
6705
- for (const namespace of role.resources[type]) {
6706
- if (seen.has(namespace)) continue;
6707
- seen.add(namespace);
6708
- namespaces[type].push(namespace);
6709
- }
6710
- }
6711
- }
6712
- return namespaces;
6713
- }
6714
- var ROLE_RESOURCE_TYPES, RoleResourceNamespacesSchema, RoleSchema, RolesManifestSchema;
6715
- var init_roles = __esm({
6716
- "src/roles.ts"() {
6717
- "use strict";
6718
- init_fs();
6719
- ROLE_RESOURCE_TYPES = ["knowledge", "skills"];
6720
- RoleResourceNamespacesSchema = z3.object({
6721
- knowledge: z3.array(z3.string().min(1)),
6722
- skills: z3.array(z3.string().min(1)),
6723
- // learnings is accepted for backward compatibility but ignored at runtime.
6724
- // All learnings are shared flat across the entire team (no namespace isolation).
6725
- learnings: z3.array(z3.string()).optional()
6726
- });
6727
- RoleSchema = z3.object({
6728
- id: z3.string().min(1),
6729
- description: z3.string().default(""),
6730
- resources: RoleResourceNamespacesSchema
6731
- });
6732
- RolesManifestSchema = z3.object({
6733
- version: z3.number(),
6734
- roles: z3.array(RoleSchema).min(1),
6735
- // defaults.shareTarget was removed: learnings are flat, no namespace routing needed.
6736
- // Old manifests with a defaults block are still parseable (z.passthrough on object level).
6737
- defaults: z3.object({}).passthrough().optional()
6738
- });
6739
- }
6740
- });
6741
-
6742
6848
  // src/utils/path-safety.ts
6743
6849
  var path_safety_exports = {};
6744
6850
  __export(path_safety_exports, {
@@ -7487,10 +7593,20 @@ async function countInterventions(transcriptPath) {
7487
7593
  const { interrupt, toolReject, toolError } = await scanTranscriptStop(transcriptPath);
7488
7594
  return { interrupt, toolReject, toolError };
7489
7595
  }
7490
- function isCorrectionPrompt(text) {
7596
+ function wordBoundaryPattern(keyword) {
7597
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7598
+ return new RegExp(`(?<![\\p{L}\\p{N}_])${escaped}(?![\\p{L}\\p{N}_])`, "u");
7599
+ }
7600
+ function containsKeyword(lower, keyword) {
7601
+ const k = keyword.trim().normalize("NFC").toLowerCase();
7602
+ if (!k) return false;
7603
+ if (UNSPACED_SCRIPT_RE.test(k)) return lower.includes(k);
7604
+ return wordBoundaryPattern(k).test(lower);
7605
+ }
7606
+ function isCorrectionPrompt(text, extraKeywords = []) {
7491
7607
  if (!text) return false;
7492
- const lower = text.toLowerCase();
7493
- return CORRECTION_KEYWORDS.some((k) => lower.includes(k));
7608
+ const lower = text.normalize("NFC").toLowerCase();
7609
+ return [...CORRECTION_KEYWORDS, ...extraKeywords].some((k) => containsKeyword(lower, k));
7494
7610
  }
7495
7611
  function mapEventType(hookEventName) {
7496
7612
  switch (hookEventName) {
@@ -7511,7 +7627,7 @@ function mapEventType(hookEventName) {
7511
7627
  return null;
7512
7628
  }
7513
7629
  }
7514
- async function parseHookEvent(raw, tool) {
7630
+ async function parseHookEvent(raw, tool, options) {
7515
7631
  if (!raw.trim()) return null;
7516
7632
  let hookData;
7517
7633
  try {
@@ -7550,6 +7666,7 @@ async function parseHookEvent(raw, tool) {
7550
7666
  }
7551
7667
  if (eventType === "prompt_submit" && typeof hookData.prompt === "string") {
7552
7668
  event.promptSummary = hookData.prompt.slice(0, 200);
7669
+ event.correction = isCorrectionPrompt(hookData.prompt, options?.correctionKeywords);
7553
7670
  }
7554
7671
  if (eventType === "stop" && typeof hookData.transcript_path === "string") {
7555
7672
  event.transcriptPath = hookData.transcript_path;
@@ -7856,7 +7973,8 @@ function aggregateSessionMetrics(events) {
7856
7973
  const stopAt = lastStopAt.get(event.sessionId);
7857
7974
  if (stopAt !== void 0) {
7858
7975
  const gap = new Date(event.timestamp).getTime() - stopAt;
7859
- if (gap >= 0 && gap <= CORRECTION_WINDOW_MS && isCorrectionPrompt(event.promptSummary)) {
7976
+ const isCorrection = event.correction ?? isCorrectionPrompt(event.promptSummary);
7977
+ if (gap >= 0 && gap <= CORRECTION_WINDOW_MS && isCorrection) {
7860
7978
  m.correction++;
7861
7979
  }
7862
7980
  lastStopAt.delete(event.sessionId);
@@ -7918,7 +8036,7 @@ async function dashboardReport(toolArg) {
7918
8036
  compactEvents().catch(() => {
7919
8037
  });
7920
8038
  }
7921
- var TRANSCRIPT_TAIL_BYTES, STOPPED_OUTPUT_MAX_CHARS, CODEX_USAGE_TAIL_BYTES, CODEX_USAGE_MAX_ATTEMPTS, CODEX_USAGE_RETRY_MS, CODEBUDDY_USAGE_MAX_ATTEMPTS, CODEBUDDY_USAGE_RETRY_MS, CODEBUDDY_BLOB_MAX_COUNT, CODEBUDDY_REJECT_MARKER;
8039
+ var TRANSCRIPT_TAIL_BYTES, STOPPED_OUTPUT_MAX_CHARS, CODEX_USAGE_TAIL_BYTES, CODEX_USAGE_MAX_ATTEMPTS, CODEX_USAGE_RETRY_MS, CODEBUDDY_USAGE_MAX_ATTEMPTS, CODEBUDDY_USAGE_RETRY_MS, CODEBUDDY_BLOB_MAX_COUNT, CODEBUDDY_REJECT_MARKER, UNSPACED_SCRIPT_RE;
7922
8040
  var init_dashboard_collector = __esm({
7923
8041
  "src/dashboard-collector.ts"() {
7924
8042
  "use strict";
@@ -7941,23 +8059,77 @@ var init_dashboard_collector = __esm({
7941
8059
  CODEBUDDY_USAGE_RETRY_MS = 250;
7942
8060
  CODEBUDDY_BLOB_MAX_COUNT = 2e3;
7943
8061
  CODEBUDDY_REJECT_MARKER = "User rejected this command";
8062
+ UNSPACED_SCRIPT_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
8063
+ }
8064
+ });
8065
+
8066
+ // src/utils/exec.ts
8067
+ import spawn from "cross-spawn";
8068
+ async function probeBinary(binary, args = ["--version"], executor = execCommand) {
8069
+ try {
8070
+ const result = await executor(binary, args, { timeoutMs: 5e3 });
8071
+ if (result.code !== 0) return "";
8072
+ return (result.stdout || result.stderr).trim();
8073
+ } catch {
8074
+ return "";
8075
+ }
8076
+ }
8077
+ function formatCommand(command, args) {
8078
+ const quote = (value) => /^[a-zA-Z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value);
8079
+ return [command, ...args.map(quote)].join(" ");
8080
+ }
8081
+ var execCommand;
8082
+ var init_exec = __esm({
8083
+ "src/utils/exec.ts"() {
8084
+ "use strict";
8085
+ execCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
8086
+ const child = spawn(command, args, {
8087
+ cwd: options.cwd,
8088
+ env: options.env,
8089
+ stdio: ["ignore", "pipe", "pipe"],
8090
+ shell: false
8091
+ });
8092
+ let stdout = "";
8093
+ let stderr = "";
8094
+ let settled = false;
8095
+ const timeoutMs = options.timeoutMs ?? 12e4;
8096
+ const finish = (callback) => {
8097
+ if (settled) return;
8098
+ settled = true;
8099
+ clearTimeout(timer);
8100
+ callback();
8101
+ };
8102
+ child.stdout?.on("data", (chunk) => {
8103
+ const text = chunk.toString();
8104
+ stdout += text;
8105
+ if (options.stream) process.stdout.write(text);
8106
+ });
8107
+ child.stderr?.on("data", (chunk) => {
8108
+ const text = chunk.toString();
8109
+ stderr += text;
8110
+ if (options.stream) process.stderr.write(text);
8111
+ });
8112
+ const timer = setTimeout(() => {
8113
+ child.kill();
8114
+ finish(() => reject(new Error(
8115
+ `Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`
8116
+ )));
8117
+ }, timeoutMs);
8118
+ child.on("error", (error) => finish(() => reject(error)));
8119
+ child.on("close", (code) => finish(() => resolve({
8120
+ stdout,
8121
+ stderr,
8122
+ code: code ?? 1
8123
+ })));
8124
+ });
7944
8125
  }
7945
8126
  });
7946
8127
 
7947
8128
  // src/agent-version.ts
7948
- import { execFile } from "child_process";
7949
8129
  import { readFile } from "fs/promises";
7950
8130
  import path22 from "path";
7951
8131
  async function execVersion(bin, args = ["--version"]) {
7952
- return new Promise((resolve) => {
7953
- execFile(bin, args, { timeout: 5e3 }, (err, stdout) => {
7954
- if (err) {
7955
- resolve("");
7956
- return;
7957
- }
7958
- resolve(stdout.trim());
7959
- });
7960
- });
8132
+ return probeBinary(bin, args);
7961
8133
  }
7962
8134
  async function readPlistVersion(appPath) {
7963
8135
  const plistPath = path22.join(appPath, "Contents", "Info.plist");
@@ -8031,6 +8203,7 @@ var init_agent_version = __esm({
8031
8203
  "src/agent-version.ts"() {
8032
8204
  "use strict";
8033
8205
  init_logger();
8206
+ init_exec();
8034
8207
  VERSION_CACHE = /* @__PURE__ */ new Map();
8035
8208
  CODEBUDDY_IDE_PATHS = [
8036
8209
  "/Applications/CodeBuddy.app",
@@ -8297,7 +8470,7 @@ var CLAUDE_TOOLS, CURSOR_TOOLS2, CODEX_TOOLS2, BUDDY_TOOLS, OPENCODE_TOOLS, MCP_
8297
8470
  var init_mcp_format = __esm({
8298
8471
  "src/resources/mcp-format.ts"() {
8299
8472
  "use strict";
8300
- CLAUDE_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "qoder", "zcode"]);
8473
+ CLAUDE_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "qoder", "kiro", "zcode"]);
8301
8474
  CURSOR_TOOLS2 = /* @__PURE__ */ new Set(["cursor"]);
8302
8475
  CODEX_TOOLS2 = /* @__PURE__ */ new Set(["codex", "codex-internal", "tcodex"]);
8303
8476
  BUDDY_TOOLS = /* @__PURE__ */ new Set(["codebuddy", "workbuddy"]);
@@ -8354,7 +8527,8 @@ function teamMcpToDef(s) {
8354
8527
  env: s.env,
8355
8528
  timeout: s.timeout,
8356
8529
  requires: s.requires,
8357
- tools: s.tools
8530
+ tools: s.tools,
8531
+ roles: s.roles
8358
8532
  };
8359
8533
  }
8360
8534
  async function parseTeamMcpServers(repoPath) {
@@ -8380,7 +8554,8 @@ var init_mcp = __esm({
8380
8554
  env: z4.record(z4.string(), z4.string()).optional(),
8381
8555
  timeout: z4.number().int().positive().optional(),
8382
8556
  requires: z4.array(z4.string()).optional(),
8383
- tools: z4.array(z4.string()).optional()
8557
+ tools: z4.array(z4.string()).optional(),
8558
+ roles: z4.array(z4.string()).optional()
8384
8559
  }).refine((s) => s.transport === "stdio" ? !!s.command : true, {
8385
8560
  message: "stdio transport requires `command`"
8386
8561
  }).refine((s) => s.transport === "stdio" ? true : !!s.url, {
@@ -8703,6 +8878,14 @@ async function reconcileMcpForConfig(teamConfig, localConfig, options = {}) {
8703
8878
  return { changes, wrote };
8704
8879
  }
8705
8880
  const excluded = new Set(localConfig.excludedSkills ?? []);
8881
+ const activeRoles = activeRoleIds(localConfig);
8882
+ if (!removeAll) {
8883
+ await warnUnknownRoleIds(
8884
+ localConfig.repo.localPath,
8885
+ "mcp.yaml",
8886
+ teamDefs.map((def) => ({ kind: "server", name: def.name, roles: def.roles }))
8887
+ );
8888
+ }
8706
8889
  const targets = await resolveMcpTargets(teamConfig, localConfig);
8707
8890
  if (targets.length === 0) return { changes, wrote };
8708
8891
  const dataHome = getDataHome(localConfig);
@@ -8726,6 +8909,7 @@ async function reconcileMcpForConfig(teamConfig, localConfig, options = {}) {
8726
8909
  const desired = /* @__PURE__ */ new Map();
8727
8910
  for (const raw of teamDefs) {
8728
8911
  if (raw.tools && !raw.tools.includes(target.tool)) continue;
8912
+ if (!matchesRoles(raw.roles, activeRoles)) continue;
8729
8913
  if (excluded.has(raw.name)) {
8730
8914
  changes.push({ tool: target.tool, server: raw.name, action: "skipped", reason: "excluded by user" });
8731
8915
  continue;
@@ -8878,6 +9062,7 @@ var init_mcp_reconcile = __esm({
8878
9062
  init_types();
8879
9063
  init_mcp_format();
8880
9064
  init_mcp();
9065
+ init_roles();
8881
9066
  init_fs();
8882
9067
  init_logger();
8883
9068
  init_mcp_manifest();
@@ -9179,7 +9364,7 @@ __export(local_agent_exports, {
9179
9364
  import fs14 from "fs";
9180
9365
  import os7 from "os";
9181
9366
  import path28 from "path";
9182
- import { execFile as execFile2 } from "child_process";
9367
+ import { execFile } from "child_process";
9183
9368
  import { promisify } from "util";
9184
9369
  import fse4 from "fs-extra";
9185
9370
  function modelAgentKind(tool) {
@@ -11418,7 +11603,7 @@ var init_local_agent = __esm({
11418
11603
  init_types();
11419
11604
  init_home();
11420
11605
  init_git2();
11421
- execFileAsync = promisify(execFile2);
11606
+ execFileAsync = promisify(execFile);
11422
11607
  LOCAL_AGENT_DIR = "local-agent";
11423
11608
  CONFIG_FILE = "config.json";
11424
11609
  MANIFEST_FILE = "manifest.json";
@@ -12315,6 +12500,7 @@ var init_skills = __esm({
12315
12500
  await this.addTombstone(name, localConfig);
12316
12501
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12317
12502
  if (!toolPath.skills) continue;
12503
+ if (isAgentExcluded(localConfig, tool)) continue;
12318
12504
  let skillDir;
12319
12505
  if (tool === "openclaw") {
12320
12506
  const wsDir = await resolveOpenclawWorkspaceDir();
@@ -12571,6 +12757,7 @@ var init_rules = __esm({
12571
12757
  await this.addTombstone(name, localConfig);
12572
12758
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12573
12759
  if (!toolPath.rules) continue;
12760
+ if (isAgentExcluded(localConfig, tool)) continue;
12574
12761
  const extensions = /* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"]);
12575
12762
  for (const extension of extensions) {
12576
12763
  const filePath = path32.join(baseDir, toolPath.rules, `${name}${extension}`);
@@ -12612,6 +12799,7 @@ var init_rules = __esm({
12612
12799
  const baseDir = resolveBaseDir(localConfig);
12613
12800
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12614
12801
  if (!toolPath.rules) continue;
12802
+ if (isAgentExcluded(localConfig, tool)) continue;
12615
12803
  if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
12616
12804
  const destDir = path32.join(baseDir, toolPath.rules);
12617
12805
  if (!await pathExists(destDir)) continue;
@@ -12815,9 +13003,9 @@ var init_env = __esm({
12815
13003
  }
12816
13004
  const envYamlPath = path34.join(localConfig.repo.localPath, "env", "env.yaml");
12817
13005
  if (!await pathExists(envYamlPath)) return [];
12818
- const { execFile: execFile5 } = await import("child_process");
13006
+ const { execFile: execFile4 } = await import("child_process");
12819
13007
  const { promisify: promisify4 } = await import("util");
12820
- const execFileAsync4 = promisify4(execFile5);
13008
+ const execFileAsync4 = promisify4(execFile4);
12821
13009
  try {
12822
13010
  await execFileAsync4("git", ["diff", "--exit-code", "env/env.yaml"], {
12823
13011
  cwd: localConfig.repo.localPath
@@ -12985,12 +13173,20 @@ import path35 from "path";
12985
13173
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
12986
13174
  import matter3 from "gray-matter";
12987
13175
  import { stringify as stringifyToml, parse as parseToml } from "smol-toml";
13176
+ function agentStemFromFilename(filename) {
13177
+ for (const ext of AGENT_FILE_EXTENSIONS) {
13178
+ if (filename.endsWith(ext)) return filename.slice(0, -ext.length);
13179
+ }
13180
+ return null;
13181
+ }
12988
13182
  function agentFileExtensionForTool(tool) {
12989
13183
  switch (tool) {
12990
13184
  case "codex":
12991
13185
  case "codex-internal":
12992
13186
  case "tcodex":
12993
13187
  return ".toml";
13188
+ case "kiro":
13189
+ return ".json";
12994
13190
  default:
12995
13191
  return ".md";
12996
13192
  }
@@ -13051,6 +13247,35 @@ function renderForJoycode(spec) {
13051
13247
  content: renderMarkdownAgent(spec, spec.tool_extras?.["joycode"])
13052
13248
  };
13053
13249
  }
13250
+ function isRecord(value) {
13251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13252
+ }
13253
+ function isManagedKiroSessionHook(value) {
13254
+ return isRecord(value) && typeof value["command"] === "string" && value["command"].includes("teamai hook-dispatch session-start --tool kiro");
13255
+ }
13256
+ function renderForKiro(spec) {
13257
+ const extras = { ...spec.tool_extras?.["kiro"] ?? {} };
13258
+ const existingHooks = isRecord(extras["hooks"]) ? { ...extras["hooks"] } : {};
13259
+ const existingAgentSpawn = Array.isArray(existingHooks["agentSpawn"]) ? existingHooks["agentSpawn"].filter((entry) => !isManagedKiroSessionHook(entry)) : [];
13260
+ existingHooks["agentSpawn"] = [
13261
+ ...existingAgentSpawn,
13262
+ { command: KIRO_SESSION_START_COMMAND }
13263
+ ];
13264
+ extras["hooks"] = existingHooks;
13265
+ const json = {
13266
+ name: spec.name,
13267
+ description: spec.description,
13268
+ prompt: spec.instructions
13269
+ };
13270
+ if (spec.model !== void 0) json["model"] = spec.model;
13271
+ if (spec.tools !== void 0 && spec.tools.length > 0) json["tools"] = spec.tools;
13272
+ Object.assign(json, extras);
13273
+ return {
13274
+ ext: agentFileExtensionForTool("kiro"),
13275
+ content: `${JSON.stringify(json, null, 2)}
13276
+ `
13277
+ };
13278
+ }
13054
13279
  function renderForCodex(spec) {
13055
13280
  return {
13056
13281
  ext: agentFileExtensionForTool("codex"),
@@ -13178,6 +13403,43 @@ function reverseFromJoycode(filePath, content) {
13178
13403
  }
13179
13404
  return { ok: true, spec };
13180
13405
  }
13406
+ function reverseFromKiro(filePath, content) {
13407
+ let parsed;
13408
+ try {
13409
+ const raw = JSON.parse(content);
13410
+ if (!isRecord(raw)) return { ok: false, reason: "agent config must be a JSON object" };
13411
+ parsed = raw;
13412
+ } catch (err) {
13413
+ return { ok: false, reason: `parse error: ${err.message}` };
13414
+ }
13415
+ const name = parsed["name"] ?? path35.basename(filePath, ".json");
13416
+ if (!name) return { ok: false, reason: "missing field name" };
13417
+ if (!parsed["description"]) return { ok: false, reason: "missing field description" };
13418
+ if (!parsed["prompt"]) return { ok: false, reason: "missing field prompt" };
13419
+ const extras = {};
13420
+ for (const [key, value] of Object.entries(parsed)) {
13421
+ if (!COMMON_KIRO_FIELDS.has(key)) extras[key] = value;
13422
+ }
13423
+ if (isRecord(extras["hooks"])) {
13424
+ const hooks = { ...extras["hooks"] };
13425
+ if (Array.isArray(hooks["agentSpawn"])) {
13426
+ const remaining = hooks["agentSpawn"].filter((entry) => !isManagedKiroSessionHook(entry));
13427
+ if (remaining.length > 0) hooks["agentSpawn"] = remaining;
13428
+ else delete hooks["agentSpawn"];
13429
+ }
13430
+ if (Object.keys(hooks).length > 0) extras["hooks"] = hooks;
13431
+ else delete extras["hooks"];
13432
+ }
13433
+ const spec = {
13434
+ name,
13435
+ description: parsed["description"],
13436
+ instructions: parsed["prompt"]
13437
+ };
13438
+ if (parsed["model"] !== void 0) spec.model = parsed["model"];
13439
+ if (parsed["tools"] !== void 0) spec.tools = parsed["tools"];
13440
+ if (Object.keys(extras).length > 0) spec.tool_extras = { kiro: extras };
13441
+ return { ok: true, spec };
13442
+ }
13181
13443
  function reverseFromCodex(filePath, content) {
13182
13444
  let parsed;
13183
13445
  try {
@@ -13326,16 +13588,19 @@ function renderForTool(spec, tool) {
13326
13588
  return renderForJoycode(spec);
13327
13589
  case "qoder":
13328
13590
  return renderForClaude(spec);
13591
+ case "kiro":
13592
+ return renderForKiro(spec);
13329
13593
  case "zcode":
13330
13594
  return renderForClaude(spec);
13331
13595
  case "opencode":
13332
13596
  return renderForOpencode(spec);
13333
13597
  }
13334
13598
  }
13335
- var ALL_SUPPORTED_TOOLS, COMMON_CLAUDE_FIELDS, COMMON_CURSOR_FIELDS, COMMON_CODEX_FIELDS, COMMON_OPENCODE_FIELDS, MERGE_COMMON_FIELDS;
13599
+ var ALL_SUPPORTED_TOOLS, AGENT_FILE_EXTENSIONS, KIRO_SESSION_START_COMMAND, COMMON_CLAUDE_FIELDS, COMMON_CURSOR_FIELDS, COMMON_CODEX_FIELDS, COMMON_KIRO_FIELDS, COMMON_OPENCODE_FIELDS, MERGE_COMMON_FIELDS;
13336
13600
  var init_agent_format = __esm({
13337
13601
  "src/resources/agent-format.ts"() {
13338
13602
  "use strict";
13603
+ init_builtin_hooks();
13339
13604
  ALL_SUPPORTED_TOOLS = [
13340
13605
  "claude",
13341
13606
  "claude-internal",
@@ -13347,12 +13612,16 @@ var init_agent_format = __esm({
13347
13612
  "cursor",
13348
13613
  "joycode",
13349
13614
  "qoder",
13615
+ "kiro",
13350
13616
  "zcode",
13351
13617
  "opencode"
13352
13618
  ];
13619
+ AGENT_FILE_EXTENSIONS = [".md", ".toml", ".json"];
13620
+ KIRO_SESSION_START_COMMAND = getDispatchCommand("session-start", "kiro");
13353
13621
  COMMON_CLAUDE_FIELDS = /* @__PURE__ */ new Set(["name", "description", "model", "tools"]);
13354
13622
  COMMON_CURSOR_FIELDS = /* @__PURE__ */ new Set(["agent_id", "description", "model", "tools"]);
13355
13623
  COMMON_CODEX_FIELDS = /* @__PURE__ */ new Set(["name", "description", "developer_instructions", "model"]);
13624
+ COMMON_KIRO_FIELDS = /* @__PURE__ */ new Set(["name", "description", "prompt", "model", "tools"]);
13356
13625
  COMMON_OPENCODE_FIELDS = /* @__PURE__ */ new Set(["description", "model"]);
13357
13626
  MERGE_COMMON_FIELDS = [
13358
13627
  "name",
@@ -13385,8 +13654,7 @@ async function removeStaleAgentSiblings(targetAgentsDir, stem, targetExt) {
13385
13654
  return;
13386
13655
  }
13387
13656
  for (const file of files) {
13388
- const base = file.replace(/\.(md|toml)$/, "");
13389
- if (base !== stem) continue;
13657
+ if (agentStemFromFilename(file) !== stem) continue;
13390
13658
  if (file === `${stem}${targetExt}`) continue;
13391
13659
  try {
13392
13660
  await remove(path36.join(targetAgentsDir, file));
@@ -13469,10 +13737,267 @@ var init_builtin_agents = __esm({
13469
13737
  }
13470
13738
  });
13471
13739
 
13472
- // src/resources/agents.ts
13740
+ // src/projects.ts
13741
+ var projects_exports = {};
13742
+ __export(projects_exports, {
13743
+ describeProjects: () => describeProjects,
13744
+ findProject: () => findProject,
13745
+ isSafeNamespaceSegment: () => isSafeNamespaceSegment,
13746
+ listProjectIds: () => listProjectIds,
13747
+ loadProjectsManifest: () => loadProjectsManifest,
13748
+ mergeNamespaces: () => mergeNamespaces,
13749
+ resolveActiveLearningsNamespaces: () => resolveActiveLearningsNamespaces,
13750
+ resolveProjectResourceNamespaces: () => resolveProjectResourceNamespaces,
13751
+ saveProjectsManifest: () => saveProjectsManifest
13752
+ });
13473
13753
  import path37 from "path";
13754
+ import YAML8 from "yaml";
13755
+ import { z as z6 } from "zod";
13756
+ function isSafeNamespaceSegment(seg) {
13757
+ return SAFE_ID.test(seg) && seg !== "." && seg !== "..";
13758
+ }
13759
+ function validateManifestShape2(raw) {
13760
+ if (!raw || typeof raw !== "object") {
13761
+ throw new Error("Invalid projects manifest: expected an object");
13762
+ }
13763
+ const candidate = raw;
13764
+ const projects = candidate.projects;
13765
+ if (projects !== void 0 && !Array.isArray(projects)) {
13766
+ throw new Error("Invalid projects manifest: projects must be an array");
13767
+ }
13768
+ for (const project of projects ?? []) {
13769
+ if (!project || typeof project !== "object") {
13770
+ throw new Error("Invalid projects manifest: every project must be an object");
13771
+ }
13772
+ const resources = project.resources;
13773
+ if (resources !== void 0 && (typeof resources !== "object" || Array.isArray(resources))) {
13774
+ throw new Error(`Invalid projects manifest: project ${project.id ?? "<unknown>"} has invalid resources`);
13775
+ }
13776
+ if (resources) {
13777
+ const ALLOWED_RESOURCE_KEYS = new Set(PROJECT_RESOURCE_TYPES);
13778
+ for (const key of Object.keys(resources)) {
13779
+ if (!ALLOWED_RESOURCE_KEYS.has(key)) {
13780
+ throw new Error(`Invalid projects manifest: unknown resource type "${key}"`);
13781
+ }
13782
+ }
13783
+ }
13784
+ }
13785
+ const manifest = ProjectsManifestSchema.parse(raw);
13786
+ const ids = /* @__PURE__ */ new Set();
13787
+ for (const project of manifest.projects) {
13788
+ if (ids.has(project.id)) {
13789
+ throw new Error(`Invalid projects manifest: duplicate project id "${project.id}"`);
13790
+ }
13791
+ ids.add(project.id);
13792
+ }
13793
+ return manifest;
13794
+ }
13795
+ async function loadProjectsManifest(repoPath) {
13796
+ const manifestPath = path37.join(repoPath, "manifest", "projects.yaml");
13797
+ const content = await readFileSafe(manifestPath);
13798
+ if (!content) {
13799
+ return null;
13800
+ }
13801
+ let raw;
13802
+ try {
13803
+ raw = YAML8.parse(content);
13804
+ } catch (error) {
13805
+ throw new Error(`Invalid projects manifest YAML: ${error.message}`);
13806
+ }
13807
+ return validateManifestShape2(raw);
13808
+ }
13809
+ async function saveProjectsManifest(repoPath, manifest) {
13810
+ validateManifestShape2(manifest);
13811
+ const manifestDir = path37.join(repoPath, "manifest");
13812
+ const manifestPath = path37.join(manifestDir, "projects.yaml");
13813
+ await ensureDir(manifestDir);
13814
+ await writeFile(manifestPath, YAML8.stringify(manifest));
13815
+ }
13816
+ function findProject(manifest, projectId) {
13817
+ return manifest.projects.find((candidate) => candidate.id === projectId);
13818
+ }
13819
+ function listProjectIds(manifest) {
13820
+ return manifest.projects.map((project) => project.id);
13821
+ }
13822
+ function describeProjects(projects) {
13823
+ return projects.map((project) => {
13824
+ const label = project.name || project.id;
13825
+ return project.description ? `${label}: ${project.description}` : label;
13826
+ });
13827
+ }
13828
+ function getProjectOrThrow(manifest, projectId) {
13829
+ const project = findProject(manifest, projectId);
13830
+ if (!project) {
13831
+ throw new Error(`Unknown project "${projectId}". Valid projects: ${listProjectIds(manifest).join(", ")}`);
13832
+ }
13833
+ return project;
13834
+ }
13835
+ function resolveProjectResourceNamespaces(input) {
13836
+ const resolved = input.activeProjects.map((id) => getProjectOrThrow(input.manifest, id));
13837
+ const namespaces = {
13838
+ knowledge: [],
13839
+ skills: [],
13840
+ learnings: [],
13841
+ agents: []
13842
+ };
13843
+ for (const type of PROJECT_RESOURCE_TYPES) {
13844
+ const seen = /* @__PURE__ */ new Set();
13845
+ for (const project of resolved) {
13846
+ for (const namespace of project.resources[type]) {
13847
+ if (seen.has(namespace)) continue;
13848
+ seen.add(namespace);
13849
+ namespaces[type].push(namespace);
13850
+ }
13851
+ }
13852
+ }
13853
+ return namespaces;
13854
+ }
13855
+ async function resolveActiveLearningsNamespaces(repoPath, activeProjects) {
13856
+ if (activeProjects.length === 0) return [];
13857
+ const manifest = await loadProjectsManifest(repoPath);
13858
+ if (!manifest) return [];
13859
+ try {
13860
+ return resolveProjectResourceNamespaces({ manifest, activeProjects }).learnings;
13861
+ } catch {
13862
+ return [];
13863
+ }
13864
+ }
13865
+ function mergeNamespaces(roleNamespaces, projectNamespaces) {
13866
+ const dedupe = (a, b) => {
13867
+ const seen = /* @__PURE__ */ new Set();
13868
+ const out = [];
13869
+ for (const ns of [...a, ...b]) {
13870
+ if (seen.has(ns)) continue;
13871
+ seen.add(ns);
13872
+ out.push(ns);
13873
+ }
13874
+ return out;
13875
+ };
13876
+ return {
13877
+ knowledge: dedupe(roleNamespaces.knowledge, projectNamespaces.knowledge),
13878
+ skills: dedupe(roleNamespaces.skills, projectNamespaces.skills),
13879
+ // Roles never contribute learnings; this is effectively the project set.
13880
+ learnings: dedupe(roleNamespaces.learnings, projectNamespaces.learnings),
13881
+ agents: dedupe(roleNamespaces.agents, projectNamespaces.agents)
13882
+ };
13883
+ }
13884
+ var PROJECT_RESOURCE_TYPES, ProjectResourceNamespacesSchema, SAFE_ID, ProjectSchema, ProjectsManifestSchema;
13885
+ var init_projects = __esm({
13886
+ "src/projects.ts"() {
13887
+ "use strict";
13888
+ init_fs();
13889
+ PROJECT_RESOURCE_TYPES = ["knowledge", "skills", "learnings", "agents"];
13890
+ ProjectResourceNamespacesSchema = z6.object({
13891
+ knowledge: z6.array(z6.string().min(1)).default([]),
13892
+ skills: z6.array(z6.string().min(1)).default([]),
13893
+ learnings: z6.array(z6.string().min(1)).default([]),
13894
+ agents: z6.array(z6.string().min(1)).default([])
13895
+ });
13896
+ SAFE_ID = /^[A-Za-z0-9._-]+$/;
13897
+ ProjectSchema = z6.object({
13898
+ id: z6.string().min(1).refine((v) => isSafeNamespaceSegment(v), {
13899
+ message: "project id must be a single path segment (letters, digits, '.', '_', '-'; no '/', '\\\\', or '..')"
13900
+ }),
13901
+ name: z6.string().default(""),
13902
+ description: z6.string().default(""),
13903
+ resources: ProjectResourceNamespacesSchema
13904
+ });
13905
+ ProjectsManifestSchema = z6.object({
13906
+ version: z6.number(),
13907
+ // Unlike roles (`.min(1)`), a repo may define zero projects — a team without
13908
+ // project partitioning simply has no projects.yaml, and an empty list is valid.
13909
+ projects: z6.array(ProjectSchema).default([])
13910
+ });
13911
+ }
13912
+ });
13913
+
13914
+ // src/resource-namespaces.ts
13915
+ async function resolveResourceNamespaces(localConfig) {
13916
+ const activeProjects = localConfig.projects ?? [];
13917
+ const primaryRole = localConfig.primaryRole;
13918
+ const hasRole = !!primaryRole;
13919
+ const hasProjects = activeProjects.length > 0;
13920
+ const projectsManifest = await loadProjectsManifest(localConfig.repo.localPath);
13921
+ const teamHasProjects = !!projectsManifest && projectsManifest.projects.length > 0;
13922
+ if (!hasRole && !hasProjects && !teamHasProjects) return null;
13923
+ let roleNamespaces = { knowledge: [], skills: [], learnings: [], agents: [] };
13924
+ let allRoleSkillNamespaces = /* @__PURE__ */ new Set();
13925
+ if (primaryRole) {
13926
+ let rolesManifest;
13927
+ try {
13928
+ rolesManifest = await loadRolesManifest(localConfig.repo.localPath);
13929
+ } catch {
13930
+ log.warn("Could not load roles manifest. Skipping role-based filtering.");
13931
+ rolesManifest = null;
13932
+ }
13933
+ if (rolesManifest) {
13934
+ try {
13935
+ roleNamespaces = resolveRoleResourceNamespaces({
13936
+ manifest: rolesManifest,
13937
+ primaryRole,
13938
+ additionalRoles: localConfig.additionalRoles ?? []
13939
+ });
13940
+ allRoleSkillNamespaces = new Set(rolesManifest.roles.flatMap((role) => role.resources.skills));
13941
+ } catch {
13942
+ log.warn(`Role "${localConfig.primaryRole}" not found in manifest. Falling back to unfiltered sync.`);
13943
+ log.warn("Run `teamai roles set <role>` to pick a valid role.");
13944
+ if (!hasProjects && !teamHasProjects) return null;
13945
+ }
13946
+ } else if (!hasProjects && !teamHasProjects) {
13947
+ return null;
13948
+ }
13949
+ }
13950
+ let projectNamespaces = { knowledge: [], skills: [], learnings: [], agents: [] };
13951
+ let allProjectSkillNamespaces = /* @__PURE__ */ new Set();
13952
+ if (projectsManifest) {
13953
+ allProjectSkillNamespaces = new Set(projectsManifest.projects.flatMap((p) => p.resources.skills));
13954
+ if (hasProjects) {
13955
+ try {
13956
+ projectNamespaces = resolveProjectResourceNamespaces({
13957
+ manifest: projectsManifest,
13958
+ activeProjects
13959
+ });
13960
+ } catch (e) {
13961
+ log.warn(`${e instanceof Error ? e.message : String(e)} Falling back to role-only filtering.`);
13962
+ }
13963
+ }
13964
+ } else if (hasProjects) {
13965
+ log.warn("Active projects configured but no projects manifest found. Skipping project-based filtering.");
13966
+ }
13967
+ const activeNamespaces = mergeNamespaces(roleNamespaces, projectNamespaces);
13968
+ const allSkillNamespaces = /* @__PURE__ */ new Set([...allRoleSkillNamespaces, ...allProjectSkillNamespaces]);
13969
+ return { activeNamespaces, allSkillNamespaces };
13970
+ }
13971
+ var init_resource_namespaces = __esm({
13972
+ "src/resource-namespaces.ts"() {
13973
+ "use strict";
13974
+ init_roles();
13975
+ init_projects();
13976
+ init_logger();
13977
+ }
13978
+ });
13979
+
13980
+ // src/resources/agents.ts
13981
+ import path38 from "path";
13474
13982
  import { isDeepStrictEqual } from "util";
13475
13983
  import { parse as parseYaml2 } from "yaml";
13984
+ async function listTeamAgentDirs(teamAgentsDir) {
13985
+ const dirs = [{ dir: teamAgentsDir }];
13986
+ for (const namespace of await listDirs(teamAgentsDir)) {
13987
+ if (isSafeNamespaceSegment(namespace)) dirs.push({ dir: path38.join(teamAgentsDir, namespace), namespace });
13988
+ }
13989
+ return dirs;
13990
+ }
13991
+ async function findTeamAgentFiles(teamAgentsDir, stem) {
13992
+ const found = [];
13993
+ for (const { dir, namespace } of await listTeamAgentDirs(teamAgentsDir)) {
13994
+ for (const ext of [".yaml", ".md"]) {
13995
+ const candidate = path38.join(dir, `${stem}${ext}`);
13996
+ if (await pathExists(candidate)) found.push({ path: candidate, ext, ...namespace ? { namespace } : {} });
13997
+ }
13998
+ }
13999
+ return found;
14000
+ }
13476
14001
  function mergeCanonicalEdits(canonical, perTool) {
13477
14002
  const merged = { ...canonical };
13478
14003
  const extras = { ...canonical.tool_extras };
@@ -13514,10 +14039,12 @@ function mergeCanonicalEdits(canonical, perTool) {
13514
14039
  else delete merged.tool_extras;
13515
14040
  return { ok: true, spec: merged };
13516
14041
  }
13517
- function getAgentStem(filename) {
13518
- if (filename.endsWith(".md")) return filename.slice(0, -3);
13519
- if (filename.endsWith(".toml")) return filename.slice(0, -5);
13520
- return null;
14042
+ async function removeStaleAgentSiblings2(agentsDir, stem, targetExt) {
14043
+ for (const file of await listFiles(agentsDir)) {
14044
+ if (agentStemFromFilename(file) !== stem || file === `${stem}${targetExt}`) continue;
14045
+ await remove(path38.join(agentsDir, file));
14046
+ log.debug(`Removed stale agent sibling ${file} for ${stem}`);
14047
+ }
13521
14048
  }
13522
14049
  function isKnownTool(tool) {
13523
14050
  return ALL_SUPPORTED_TOOLS.includes(tool);
@@ -13529,7 +14056,7 @@ async function agentContentEqual(tool, localPath, teamPath) {
13529
14056
  const canonicalContent = await readFileSafe(teamPath);
13530
14057
  const localContent = await readFileSafe(localPath);
13531
14058
  if (canonicalContent === null || localContent === null) return false;
13532
- const parsed = parseAgentYaml(canonicalContent, path37.basename(teamPath));
14059
+ const parsed = parseAgentYaml(canonicalContent, path38.basename(teamPath));
13533
14060
  if (!parsed.ok) return false;
13534
14061
  return localContent === renderForTool(parsed.spec, tool).content;
13535
14062
  }
@@ -13551,13 +14078,15 @@ function reverseByTool(tool, filePath, content) {
13551
14078
  return reverseFromJoycode(filePath, content);
13552
14079
  case "qoder":
13553
14080
  return reverseFromClaude(filePath, content);
14081
+ case "kiro":
14082
+ return reverseFromKiro(filePath, content);
13554
14083
  case "zcode":
13555
14084
  return reverseFromClaude(filePath, content);
13556
14085
  case "opencode":
13557
14086
  return reverseFromOpencode(filePath, content);
13558
14087
  }
13559
14088
  }
13560
- var AgentsHandler;
14089
+ var AgentsHandler, LEGACY_MD_TOOLS;
13561
14090
  var init_agents = __esm({
13562
14091
  "src/resources/agents.ts"() {
13563
14092
  "use strict";
@@ -13566,6 +14095,9 @@ var init_agents = __esm({
13566
14095
  init_logger();
13567
14096
  init_types();
13568
14097
  init_builtin_agents();
14098
+ init_resource_namespaces();
14099
+ init_projects();
14100
+ init_path_safety();
13569
14101
  init_agent_format();
13570
14102
  AgentsHandler = class extends ResourceHandler {
13571
14103
  type = "agents";
@@ -13577,50 +14109,53 @@ var init_agents = __esm({
13577
14109
  * Built-in CLI agents are excluded from push.
13578
14110
  */
13579
14111
  async scanLocalForPush(teamConfig, localConfig) {
13580
- const teamAgentsDir = path37.join(localConfig.repo.localPath, "agents");
14112
+ const teamAgentsDir = path38.join(localConfig.repo.localPath, "agents");
13581
14113
  const tombstones = await this.readTombstones(localConfig);
13582
14114
  const baseDir = resolveBaseDir(localConfig);
13583
14115
  const directItems = [];
13584
14116
  const directStems = /* @__PURE__ */ new Set();
13585
14117
  if (isSelfMode(localConfig) && localConfig.projectRoot) {
13586
- const activeAgentsDir = path37.join(localConfig.projectRoot, ".teamai", "agents");
14118
+ const activeAgentsDir = path38.join(localConfig.projectRoot, ".teamai", "agents");
13587
14119
  if (await pathExists(activeAgentsDir)) {
13588
- for (const file of await listFiles(activeAgentsDir)) {
13589
- const isYaml = file.endsWith(".yaml");
13590
- const isMd = file.endsWith(".md");
13591
- if (!isYaml && !isMd) continue;
13592
- const stem = file.replace(/\.(yaml|md)$/, "");
13593
- if (tombstones.has(stem)) continue;
13594
- if (BUILTIN_AGENT_NAMES.has(stem)) continue;
13595
- const activePath = path37.join(activeAgentsDir, file);
13596
- const basePath = path37.join(teamAgentsDir, file);
13597
- const baseExists = await pathExists(basePath);
13598
- if (baseExists && await fileContentEqual(activePath, basePath)) continue;
13599
- directItems.push({
13600
- name: stem,
13601
- type: "agents",
13602
- sourcePath: activePath,
13603
- relativePath: `agents/${file}`,
13604
- status: baseExists ? "modified" : "new",
13605
- legacy: isMd
13606
- });
13607
- directStems.add(stem);
14120
+ for (const { dir, namespace } of await listTeamAgentDirs(activeAgentsDir)) {
14121
+ const relDir = namespace ? `agents/${namespace}` : "agents";
14122
+ for (const file of await listFiles(dir)) {
14123
+ const isYaml = file.endsWith(".yaml");
14124
+ const isMd = file.endsWith(".md");
14125
+ if (!isYaml && !isMd) continue;
14126
+ const stem = file.replace(/\.(yaml|md)$/, "");
14127
+ if (tombstones.has(stem)) continue;
14128
+ if (BUILTIN_AGENT_NAMES.has(stem)) continue;
14129
+ const activePath = path38.join(dir, file);
14130
+ const basePath = path38.join(localConfig.repo.localPath, relDir, file);
14131
+ const baseExists = await pathExists(basePath);
14132
+ if (baseExists && await fileContentEqual(activePath, basePath)) continue;
14133
+ directItems.push({
14134
+ name: stem,
14135
+ type: "agents",
14136
+ sourcePath: activePath,
14137
+ relativePath: `${relDir}/${file}`,
14138
+ status: baseExists ? "modified" : "new",
14139
+ legacy: isMd
14140
+ });
14141
+ directStems.add(stem);
14142
+ }
13608
14143
  }
13609
14144
  }
13610
14145
  }
13611
14146
  const grouped = /* @__PURE__ */ new Map();
13612
14147
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13613
14148
  if (!toolPath.agents) continue;
13614
- const agentsDir = path37.join(baseDir, toolPath.agents);
14149
+ const agentsDir = path38.join(baseDir, toolPath.agents);
13615
14150
  if (!await pathExists(agentsDir)) continue;
13616
14151
  const files = await listFiles(agentsDir);
13617
14152
  for (const file of files) {
13618
- const stem = getAgentStem(file);
14153
+ const stem = agentStemFromFilename(file);
13619
14154
  if (stem === null) continue;
13620
14155
  if (tombstones.has(stem)) continue;
13621
14156
  if (BUILTIN_AGENT_NAMES.has(stem)) continue;
13622
14157
  if (directStems.has(stem)) continue;
13623
- const filePath = path37.join(agentsDir, file);
14158
+ const filePath = path38.join(agentsDir, file);
13624
14159
  let toolGroup = grouped.get(stem);
13625
14160
  if (!toolGroup) {
13626
14161
  toolGroup = /* @__PURE__ */ new Map();
@@ -13632,11 +14167,41 @@ var init_agents = __esm({
13632
14167
  }
13633
14168
  }
13634
14169
  const items = [...directItems];
14170
+ const resolved = await resolveResourceNamespaces(localConfig);
14171
+ const activeNamespaces = resolved?.activeNamespaces.agents ?? null;
13635
14172
  for (const [stem, toolFiles] of grouped) {
13636
- const teamYamlPath = path37.join(teamAgentsDir, `${stem}.yaml`);
13637
- const teamMdPath = path37.join(teamAgentsDir, `${stem}.md`);
13638
- const hasTeamYaml = await pathExists(teamYamlPath);
13639
- const hasTeamMd = await pathExists(teamMdPath);
14173
+ const sources = await findTeamAgentFiles(teamAgentsDir, stem);
14174
+ const candidates = sources.filter(
14175
+ (file) => activeNamespaces === null || !file.namespace || activeNamespaces.includes(file.namespace)
14176
+ );
14177
+ if (candidates.length > 1) {
14178
+ items.push({
14179
+ name: stem,
14180
+ type: "agents",
14181
+ sourcePath: teamAgentsDir,
14182
+ relativePath: `agents/${stem}.yaml`,
14183
+ status: "modified",
14184
+ skipReason: `Ambiguous agent "${stem}": multiple active sources (${candidates.map((file) => file.path).join(", ")}). Give active agents unique names before pushing.`
14185
+ });
14186
+ continue;
14187
+ }
14188
+ if (sources.length > 0 && candidates.length === 0) {
14189
+ items.push({
14190
+ name: stem,
14191
+ type: "agents",
14192
+ sourcePath: teamAgentsDir,
14193
+ relativePath: `agents/${stem}.yaml`,
14194
+ status: "modified",
14195
+ skipReason: `Agent "${stem}" has no active source. Activate its role or project before pushing local edits.`
14196
+ });
14197
+ continue;
14198
+ }
14199
+ const located = candidates[0];
14200
+ const teamYamlPath = located?.ext === ".yaml" ? located.path : path38.join(teamAgentsDir, `${stem}.yaml`);
14201
+ const teamMdPath = located?.ext === ".md" ? located.path : path38.join(teamAgentsDir, `${stem}.md`);
14202
+ const hasTeamYaml = located?.ext === ".yaml";
14203
+ const hasTeamMd = located?.ext === ".md";
14204
+ const teamDir = located?.namespace ? `agents/${located.namespace}` : "agents";
13640
14205
  let canonicalSpec;
13641
14206
  if (hasTeamYaml) {
13642
14207
  const raw = await readFileSafe(teamYamlPath);
@@ -13646,7 +14211,7 @@ var init_agents = __esm({
13646
14211
  name: stem,
13647
14212
  type: "agents",
13648
14213
  sourcePath: teamYamlPath,
13649
- relativePath: `agents/${stem}.yaml`,
14214
+ relativePath: `${teamDir}/${stem}.yaml`,
13650
14215
  status: "modified",
13651
14216
  skipReason: "cannot read or parse canonical agent YAML"
13652
14217
  });
@@ -13724,7 +14289,7 @@ var init_agents = __esm({
13724
14289
  name: stem,
13725
14290
  type: "agents",
13726
14291
  sourcePath: bestPath,
13727
- relativePath: `agents/${stem}.yaml`,
14292
+ relativePath: `${teamDir}/${stem}.yaml`,
13728
14293
  status: status2,
13729
14294
  mergedSpec: mergeResult.spec
13730
14295
  });
@@ -13735,7 +14300,7 @@ var init_agents = __esm({
13735
14300
  name: stem,
13736
14301
  type: "agents",
13737
14302
  sourcePath: bestPath,
13738
- relativePath: `agents/${stem}.md`,
14303
+ relativePath: `${teamDir}/${stem}.md`,
13739
14304
  status: status2,
13740
14305
  skipReason
13741
14306
  });
@@ -13746,32 +14311,33 @@ var init_agents = __esm({
13746
14311
  * Scan team repo `agents/` for files to pull.
13747
14312
  * Recognizes both *.yaml (new) and *.md (legacy).
13748
14313
  * Hidden files (tombstones) are filtered out by listFiles.
14314
+ *
14315
+ * Root-level files are shared with everyone. One level of subdirectories
14316
+ * (`agents/<namespace>/`) carries role/project-scoped agents, the same
14317
+ * convention `rules/<namespace>/` uses; pull filters them by the active
14318
+ * `agents` namespaces. Deeper nesting is not scanned.
13749
14319
  */
13750
14320
  async scanTeamForPull(_teamConfig, localConfig) {
13751
- const agentsDir = path37.join(localConfig.repo.localPath, "agents");
14321
+ const agentsDir = path38.join(localConfig.repo.localPath, "agents");
13752
14322
  if (!await pathExists(agentsDir)) return [];
13753
- const files = await listFiles(agentsDir);
13754
14323
  const items = [];
13755
- for (const file of files) {
13756
- if (file.endsWith(".yaml")) {
13757
- const stem = file.replace(/\.yaml$/, "");
14324
+ const scanDir = async (dir, namespace) => {
14325
+ const prefix = namespace ? `agents/${namespace}` : "agents";
14326
+ for (const file of await listFiles(dir)) {
14327
+ const legacy = file.endsWith(".md");
14328
+ if (!legacy && !file.endsWith(".yaml")) continue;
13758
14329
  items.push({
13759
- name: stem,
14330
+ name: file.replace(/\.(yaml|md)$/, ""),
13760
14331
  type: "agents",
13761
- sourcePath: path37.join(agentsDir, file),
13762
- relativePath: `agents/${file}`,
13763
- legacy: false
13764
- });
13765
- } else if (file.endsWith(".md")) {
13766
- const stem = file.replace(/\.md$/, "");
13767
- items.push({
13768
- name: stem,
13769
- type: "agents",
13770
- sourcePath: path37.join(agentsDir, file),
13771
- relativePath: `agents/${file}`,
13772
- legacy: true
14332
+ sourcePath: path38.join(dir, file),
14333
+ relativePath: `${prefix}/${file}`,
14334
+ legacy,
14335
+ ...namespace ? { namespace } : {}
13773
14336
  });
13774
14337
  }
14338
+ };
14339
+ for (const { dir, namespace } of await listTeamAgentDirs(agentsDir)) {
14340
+ await scanDir(dir, namespace);
13775
14341
  }
13776
14342
  return items;
13777
14343
  }
@@ -13784,22 +14350,27 @@ var init_agents = __esm({
13784
14350
  async pushItem(item, _teamConfig, localConfig) {
13785
14351
  const agentItem = item;
13786
14352
  if (agentItem.skipReason) {
13787
- log.warn(`[agents] \u8DF3\u8FC7 ${item.name}: ${agentItem.skipReason}`);
13788
- log.warn(" \u5EFA\u8BAE\u4FEE\u6539\u540E\u91CD\u65B0 push \u8BE5 subagent");
14353
+ log.warn(`[agents] Skipped ${item.name}: ${agentItem.skipReason}`);
13789
14354
  return;
13790
14355
  }
14356
+ const teamDir = path38.resolve(localConfig.repo.localPath, path38.dirname(item.relativePath));
14357
+ assertWithinRoot(
14358
+ path38.join(localConfig.repo.localPath, "agents"),
14359
+ teamDir,
14360
+ `Invalid agent destination outside team repo agents directory: ${item.relativePath}`
14361
+ );
13791
14362
  if (agentItem.mergedSpec) {
13792
- const dest2 = path37.join(localConfig.repo.localPath, "agents", `${item.name}.yaml`);
13793
- await ensureDir(path37.dirname(dest2));
14363
+ const dest2 = path38.join(teamDir, `${item.name}.yaml`);
14364
+ await ensureDir(path38.dirname(dest2));
13794
14365
  const yamlContent = serializeAgentYaml(agentItem.mergedSpec);
13795
14366
  await writeFile(dest2, yamlContent);
13796
14367
  log.debug(`Wrote agent ${item.name} \u2192 team repo (YAML format)`);
13797
14368
  return;
13798
14369
  }
13799
14370
  const ext = item.sourcePath.endsWith(".yaml") ? ".yaml" : ".md";
13800
- const dest = path37.join(localConfig.repo.localPath, "agents", `${item.name}${ext}`);
14371
+ const dest = path38.join(teamDir, `${item.name}${ext}`);
13801
14372
  if (item.sourcePath !== dest) {
13802
- await ensureDir(path37.dirname(dest));
14373
+ await ensureDir(path38.dirname(dest));
13803
14374
  await copyFile(item.sourcePath, dest);
13804
14375
  }
13805
14376
  log.debug(`Copied agent ${item.name} \u2192 team repo (${ext} verbatim)`);
@@ -13843,11 +14414,12 @@ var init_agents = __esm({
13843
14414
  continue;
13844
14415
  }
13845
14416
  if (isAgentExcluded(localConfig, tool)) continue;
13846
- const destDir = path37.join(baseDir, toolPath.agents);
14417
+ const destDir = path38.join(baseDir, toolPath.agents);
13847
14418
  try {
13848
14419
  await ensureDir(destDir);
13849
14420
  const { ext, content: rendered } = renderForTool(spec, tool);
13850
- const dest = path37.join(destDir, `${item.name}${ext}`);
14421
+ await removeStaleAgentSiblings2(destDir, item.name, ext);
14422
+ const dest = path38.join(destDir, `${item.name}${ext}`);
13851
14423
  await writeFile(dest, rendered);
13852
14424
  log.debug(`Rendered agent ${item.name} \u2192 ${tool} (${ext})`);
13853
14425
  } catch (e) {
@@ -13863,19 +14435,17 @@ var init_agents = __esm({
13863
14435
  async removeItem(name, teamConfig, localConfig) {
13864
14436
  const removed = [];
13865
14437
  const baseDir = resolveBaseDir(localConfig);
13866
- const teamAgentsDir = path37.join(localConfig.repo.localPath, "agents");
13867
- for (const ext of [".yaml", ".md"]) {
13868
- const teamFile = path37.join(teamAgentsDir, `${name}${ext}`);
13869
- if (await pathExists(teamFile)) {
13870
- await remove(teamFile);
13871
- removed.push(teamFile);
13872
- }
14438
+ const teamAgentsDir = path38.join(localConfig.repo.localPath, "agents");
14439
+ for (const located of await findTeamAgentFiles(teamAgentsDir, name)) {
14440
+ await remove(located.path);
14441
+ removed.push(located.path);
13873
14442
  }
13874
14443
  await this.addTombstone(name, localConfig);
13875
14444
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13876
14445
  if (!toolPath.agents) continue;
13877
- for (const ext of [".md", ".toml"]) {
13878
- const filePath = path37.join(baseDir, toolPath.agents, `${name}${ext}`);
14446
+ if (isAgentExcluded(localConfig, tool)) continue;
14447
+ for (const ext of AGENT_FILE_EXTENSIONS) {
14448
+ const filePath = path38.join(baseDir, toolPath.agents, `${name}${ext}`);
13879
14449
  if (await pathExists(filePath)) {
13880
14450
  await remove(filePath);
13881
14451
  removed.push(filePath);
@@ -13885,14 +14455,69 @@ var init_agents = __esm({
13885
14455
  }
13886
14456
  return removed;
13887
14457
  }
14458
+ /**
14459
+ * Revocation pass for role/project scoping. Removes the deployed copies of
14460
+ * every agent whose namespace is no longer active, on every installed tool.
14461
+ *
14462
+ * Data-safety gate, same as inactive skills: a file is deleted only when it
14463
+ * is byte-equal to what pull would render from the team source. A local edit
14464
+ * is kept and reported so nothing unpushed is lost. Root-level agents and
14465
+ * agents still deployed to the same tool destination never qualify.
14466
+ */
14467
+ async cleanupInactiveNamespaces(teamConfig, localConfig, activeNamespaces) {
14468
+ const items = await this.scanTeamForPull(teamConfig, localConfig);
14469
+ const isActive = (item) => !item.namespace || activeNamespaces.includes(item.namespace);
14470
+ const active = items.filter(isActive);
14471
+ const inactive = items.filter((item) => !isActive(item) && !BUILTIN_AGENT_NAMES.has(item.name));
14472
+ if (inactive.length === 0) return;
14473
+ const baseDir = resolveBaseDir(localConfig);
14474
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
14475
+ if (!toolPath.agents || !isKnownTool(tool) || isAgentExcluded(localConfig, tool)) continue;
14476
+ if (!await ResourceHandler.isToolInstalled(toolPath.agents, baseDir)) continue;
14477
+ const destDir = path38.join(baseDir, toolPath.agents);
14478
+ const activeDestinations = /* @__PURE__ */ new Set();
14479
+ for (const item of active) {
14480
+ const rendered = await this.renderedForTool(item, tool);
14481
+ if (rendered) activeDestinations.add(`${item.name}${rendered.ext}`);
14482
+ }
14483
+ for (const item of inactive) {
14484
+ const expected = await this.renderedForTool(item, tool);
14485
+ if (!expected || activeDestinations.has(`${item.name}${expected.ext}`)) continue;
14486
+ const deployed = path38.join(destDir, `${item.name}${expected.ext}`);
14487
+ const current = await readFileSafe(deployed);
14488
+ if (current === null) continue;
14489
+ if (current !== expected.content) {
14490
+ log.warn(`[${localConfig.scope}] Kept agent "${item.name}" (${tool}): it differs from the team source ${item.relativePath}. Back it up, then delete it manually.`);
14491
+ continue;
14492
+ }
14493
+ await remove(deployed);
14494
+ log.debug(`[${localConfig.scope}] Removed inactive role-scoped agent ${item.name} from ${tool}`);
14495
+ }
14496
+ }
14497
+ }
14498
+ /**
14499
+ * What `pullItem` writes for this agent on this tool, or null when the tool
14500
+ * is not a target (legacy `.md` only reaches LEGACY_MD_TOOLS, a YAML spec
14501
+ * honours `targets`, an unparsable spec is skipped like pull skips it).
14502
+ */
14503
+ async renderedForTool(item, tool) {
14504
+ const content = await readFileSafe(item.sourcePath);
14505
+ if (content === null) return null;
14506
+ if (item.legacy) {
14507
+ return LEGACY_MD_TOOLS.has(tool) ? { ext: ".md", content } : null;
14508
+ }
14509
+ const parsed = parseAgentYaml(content, `${item.name}.yaml`);
14510
+ if (!parsed.ok) return null;
14511
+ if (parsed.spec.targets && !parsed.spec.targets.includes(tool)) return null;
14512
+ return renderForTool(parsed.spec, tool);
14513
+ }
13888
14514
  // ─── Private helpers ──────────────────────────────────────────────────────
13889
14515
  /**
13890
14516
  * Legacy pull: copies .md as-is to Claude-compatible tools, including JoyCode.
13891
14517
  */
13892
14518
  async pullLegacyMd(item, teamConfig, baseDir, localConfig) {
13893
- const legacyTools = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
13894
14519
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13895
- if (!legacyTools.has(tool)) continue;
14520
+ if (!LEGACY_MD_TOOLS.has(tool)) continue;
13896
14521
  if (!toolPath.agents) {
13897
14522
  log.debug(`Skipping legacy agent sync for ${tool}: no agents path configured`);
13898
14523
  continue;
@@ -13902,10 +14527,10 @@ var init_agents = __esm({
13902
14527
  continue;
13903
14528
  }
13904
14529
  if (isAgentExcluded(localConfig, tool)) continue;
13905
- const destDir = path37.join(baseDir, toolPath.agents);
14530
+ const destDir = path38.join(baseDir, toolPath.agents);
13906
14531
  try {
13907
14532
  await ensureDir(destDir);
13908
- const dest = path37.join(destDir, `${item.name}.md`);
14533
+ const dest = path38.join(destDir, `${item.name}.md`);
13909
14534
  await copyFile(item.sourcePath, dest);
13910
14535
  log.debug(`Synced legacy agent ${item.name} \u2192 ${tool}`);
13911
14536
  } catch (e) {
@@ -13914,6 +14539,7 @@ var init_agents = __esm({
13914
14539
  }
13915
14540
  }
13916
14541
  };
14542
+ LEGACY_MD_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
13917
14543
  }
13918
14544
  });
13919
14545
 
@@ -13979,11 +14605,11 @@ __export(update_exports, {
13979
14605
  resolveRegistryForPackage: () => resolveRegistryForPackage,
13980
14606
  update: () => update
13981
14607
  });
13982
- import { execFile as execFile3 } from "child_process";
14608
+ import { execFile as execFile2 } from "child_process";
13983
14609
  import { promisify as promisify2 } from "util";
13984
14610
  import { randomUUID as randomUUID2 } from "crypto";
13985
14611
  import fs16 from "fs";
13986
- import path38 from "path";
14612
+ import path39 from "path";
13987
14613
  import { fileURLToPath as fileURLToPath4 } from "url";
13988
14614
  import fse7 from "fs-extra";
13989
14615
  function resolveRegistryForPackage(pkgName) {
@@ -13993,15 +14619,15 @@ function resolveRegistryForPackage(pkgName) {
13993
14619
  return PUBLIC_REGISTRY;
13994
14620
  }
13995
14621
  function resolveNpmCommand() {
13996
- const nodeDir = path38.dirname(process.execPath);
14622
+ const nodeDir = path39.dirname(process.execPath);
13997
14623
  const candidates = [
13998
14624
  // Bundled runtimes: npm installed flat next to node.exe.
13999
- path38.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
14625
+ path39.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
14000
14626
  // POSIX layout rooted at the node dir itself.
14001
- path38.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
14627
+ path39.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
14002
14628
  // Canonical POSIX install (official tarball, Homebrew, nvm): node lives in
14003
14629
  // <prefix>/bin with npm at <prefix>/lib/node_modules — one level up.
14004
- path38.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
14630
+ path39.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
14005
14631
  ];
14006
14632
  for (const c of candidates) {
14007
14633
  if (fs16.existsSync(c)) return { cmd: process.execPath, args: [c] };
@@ -14010,16 +14636,16 @@ function resolveNpmCommand() {
14010
14636
  return { cmd: "npm", args: [] };
14011
14637
  }
14012
14638
  function prefixFromEntryPath(entry, posix) {
14013
- const marker = `${path38.sep}node_modules${path38.sep}`;
14639
+ const marker = `${path39.sep}node_modules${path39.sep}`;
14014
14640
  const idx = entry.lastIndexOf(marker);
14015
14641
  if (idx <= 0) return null;
14016
14642
  const root = entry.slice(0, idx);
14017
14643
  const pkgDir = getCurrentPackageName();
14018
- if (posix && path38.basename(root) === "lib") {
14019
- const prefix = path38.dirname(root);
14020
- return fs16.existsSync(path38.join(prefix, "lib", "node_modules", pkgDir)) ? { prefix, global: true } : null;
14644
+ if (posix && path39.basename(root) === "lib") {
14645
+ const prefix = path39.dirname(root);
14646
+ return fs16.existsSync(path39.join(prefix, "lib", "node_modules", pkgDir)) ? { prefix, global: true } : null;
14021
14647
  }
14022
- return fs16.existsSync(path38.join(root, "node_modules", pkgDir)) ? { prefix: root, global: !posix } : null;
14648
+ return fs16.existsSync(path39.join(root, "node_modules", pkgDir)) ? { prefix: root, global: !posix } : null;
14023
14649
  }
14024
14650
  function resolveInstallPrefix() {
14025
14651
  return prefixFromEntryPath(fileURLToPath4(import.meta.url), process.platform !== "win32");
@@ -14032,7 +14658,7 @@ async function fetchLatestVersion(registry, timeout = VERSION_CHECK_TIMEOUT) {
14032
14658
  const { stdout } = await execFileAsync2(
14033
14659
  npm.cmd,
14034
14660
  [...npm.args, "view", pkgName, "version", `--registry=${resolvedRegistry}`],
14035
- { timeout, encoding: "utf-8" }
14661
+ { timeout, encoding: "utf-8", windowsHide: true }
14036
14662
  );
14037
14663
  const version2 = stdout.trim();
14038
14664
  if (!version2) return null;
@@ -14144,7 +14770,7 @@ async function acquireLock(lockPath) {
14144
14770
  owner
14145
14771
  });
14146
14772
  try {
14147
- await ensureDir(path38.dirname(resolved));
14773
+ await ensureDir(path39.dirname(resolved));
14148
14774
  } catch {
14149
14775
  return false;
14150
14776
  }
@@ -14266,19 +14892,19 @@ async function doUpdate() {
14266
14892
  ...target ? [`--prefix=${target.prefix}`] : [],
14267
14893
  `--registry=${registry}`
14268
14894
  ],
14269
- { timeout: INSTALL_TIMEOUT }
14895
+ { timeout: INSTALL_TIMEOUT, windowsHide: true }
14270
14896
  );
14271
14897
  log.success(`Updated teamai to v${result.latest}`);
14272
14898
  const entry = resolveTeamaiEntryScript();
14273
14899
  if (entry) {
14274
14900
  try {
14275
14901
  const installed = JSON.parse(fs16.readFileSync(
14276
- path38.join(path38.dirname(path38.dirname(entry)), "package.json"),
14902
+ path39.join(path39.dirname(path39.dirname(entry)), "package.json"),
14277
14903
  "utf-8"
14278
14904
  ));
14279
14905
  if (installed.version !== result.latest) {
14280
14906
  log.warn(
14281
- `The running install at ${path38.dirname(path38.dirname(entry))} is still v${installed.version ?? "unknown"} (expected v${result.latest}) \u2014 it may need a manual update.`
14907
+ `The running install at ${path39.dirname(path39.dirname(entry))} is still v${installed.version ?? "unknown"} (expected v${result.latest}) \u2014 it may need a manual update.`
14282
14908
  );
14283
14909
  }
14284
14910
  } catch {
@@ -14287,7 +14913,8 @@ async function doUpdate() {
14287
14913
  try {
14288
14914
  const refresh = entry ? { cmd: process.execPath, args: [entry, "hooks", "inject", "--silent"] } : { cmd: "teamai", args: ["hooks", "inject", "--silent"] };
14289
14915
  await execFileAsync2(refresh.cmd, refresh.args, {
14290
- timeout: 15e3
14916
+ timeout: 15e3,
14917
+ windowsHide: true
14291
14918
  });
14292
14919
  log.success("Refreshed hooks with new version");
14293
14920
  } catch (e) {
@@ -14331,7 +14958,7 @@ var init_update = __esm({
14331
14958
  init_types();
14332
14959
  init_prompt();
14333
14960
  init_package_info();
14334
- execFileAsync2 = promisify2(execFile3);
14961
+ execFileAsync2 = promisify2(execFile2);
14335
14962
  PUBLIC_REGISTRY = "https://registry.npmjs.org";
14336
14963
  TNPM_REGISTRY = "http://r.tnpm.oa.com";
14337
14964
  VERSION_CHECK_TIMEOUT = 5e3;
@@ -14346,9 +14973,9 @@ var marketplace_exports = {};
14346
14973
  __export(marketplace_exports, {
14347
14974
  refreshMarketplace: () => refreshMarketplace
14348
14975
  });
14349
- import path39 from "path";
14976
+ import path40 from "path";
14350
14977
  async function extractSkillDescription2(skillDir) {
14351
- const skillMdPath = path39.join(skillDir, SKILL_MD2);
14978
+ const skillMdPath = path40.join(skillDir, SKILL_MD2);
14352
14979
  const content = await readFileSafe(skillMdPath);
14353
14980
  if (!content) return "";
14354
14981
  const { data } = parseFrontmatter(content);
@@ -14357,7 +14984,7 @@ async function extractSkillDescription2(skillDir) {
14357
14984
  return String(desc).replace(/\s+/g, " ").trim();
14358
14985
  }
14359
14986
  async function refreshMarketplace(repoPath) {
14360
- const marketplacePath = path39.join(repoPath, MARKETPLACE_PATH);
14987
+ const marketplacePath = path40.join(repoPath, MARKETPLACE_PATH);
14361
14988
  if (!await pathExists(marketplacePath)) {
14362
14989
  return false;
14363
14990
  }
@@ -14371,11 +14998,11 @@ async function refreshMarketplace(repoPath) {
14371
14998
  return false;
14372
14999
  }
14373
15000
  if (!Array.isArray(marketplace.plugins)) return false;
14374
- const skillsDir = path39.join(repoPath, "skills");
15001
+ const skillsDir = path40.join(repoPath, "skills");
14375
15002
  const currentSkills = /* @__PURE__ */ new Set();
14376
15003
  const dirs = await listDirs(skillsDir);
14377
15004
  for (const dir of dirs) {
14378
- const hasSkillMd = await pathExists(path39.join(skillsDir, dir, SKILL_MD2));
15005
+ const hasSkillMd = await pathExists(path40.join(skillsDir, dir, SKILL_MD2));
14379
15006
  if (hasSkillMd) {
14380
15007
  currentSkills.add(dir);
14381
15008
  }
@@ -14394,7 +15021,7 @@ async function refreshMarketplace(repoPath) {
14394
15021
  for (const skillName of currentSkills) {
14395
15022
  const existing = existingPlugins.get(skillName);
14396
15023
  if (existing) {
14397
- const desc = await extractSkillDescription2(path39.join(skillsDir, skillName));
15024
+ const desc = await extractSkillDescription2(path40.join(skillsDir, skillName));
14398
15025
  if (desc && desc !== existing.description) {
14399
15026
  existing.description = desc;
14400
15027
  changed = true;
@@ -14402,7 +15029,7 @@ async function refreshMarketplace(repoPath) {
14402
15029
  updatedPlugins.push(existing);
14403
15030
  existingPlugins.delete(skillName);
14404
15031
  } else {
14405
- const desc = await extractSkillDescription2(path39.join(skillsDir, skillName));
15032
+ const desc = await extractSkillDescription2(path40.join(skillsDir, skillName));
14406
15033
  updatedPlugins.push({
14407
15034
  name: skillName,
14408
15035
  source: `./skills/${skillName}`,
@@ -14439,177 +15066,6 @@ var init_marketplace = __esm({
14439
15066
  }
14440
15067
  });
14441
15068
 
14442
- // src/projects.ts
14443
- var projects_exports = {};
14444
- __export(projects_exports, {
14445
- describeProjects: () => describeProjects,
14446
- findProject: () => findProject,
14447
- isSafeNamespaceSegment: () => isSafeNamespaceSegment,
14448
- listProjectIds: () => listProjectIds,
14449
- loadProjectsManifest: () => loadProjectsManifest,
14450
- mergeNamespaces: () => mergeNamespaces,
14451
- resolveActiveLearningsNamespaces: () => resolveActiveLearningsNamespaces,
14452
- resolveProjectResourceNamespaces: () => resolveProjectResourceNamespaces,
14453
- saveProjectsManifest: () => saveProjectsManifest
14454
- });
14455
- import path40 from "path";
14456
- import YAML8 from "yaml";
14457
- import { z as z6 } from "zod";
14458
- function isSafeNamespaceSegment(seg) {
14459
- return SAFE_ID.test(seg) && seg !== "." && seg !== "..";
14460
- }
14461
- function validateManifestShape2(raw) {
14462
- if (!raw || typeof raw !== "object") {
14463
- throw new Error("Invalid projects manifest: expected an object");
14464
- }
14465
- const candidate = raw;
14466
- const projects = candidate.projects;
14467
- if (projects !== void 0 && !Array.isArray(projects)) {
14468
- throw new Error("Invalid projects manifest: projects must be an array");
14469
- }
14470
- for (const project of projects ?? []) {
14471
- if (!project || typeof project !== "object") {
14472
- throw new Error("Invalid projects manifest: every project must be an object");
14473
- }
14474
- const resources = project.resources;
14475
- if (resources !== void 0 && (typeof resources !== "object" || Array.isArray(resources))) {
14476
- throw new Error(`Invalid projects manifest: project ${project.id ?? "<unknown>"} has invalid resources`);
14477
- }
14478
- if (resources) {
14479
- const ALLOWED_RESOURCE_KEYS = new Set(PROJECT_RESOURCE_TYPES);
14480
- for (const key of Object.keys(resources)) {
14481
- if (!ALLOWED_RESOURCE_KEYS.has(key)) {
14482
- throw new Error(`Invalid projects manifest: unknown resource type "${key}"`);
14483
- }
14484
- }
14485
- }
14486
- }
14487
- const manifest = ProjectsManifestSchema.parse(raw);
14488
- const ids = /* @__PURE__ */ new Set();
14489
- for (const project of manifest.projects) {
14490
- if (ids.has(project.id)) {
14491
- throw new Error(`Invalid projects manifest: duplicate project id "${project.id}"`);
14492
- }
14493
- ids.add(project.id);
14494
- }
14495
- return manifest;
14496
- }
14497
- async function loadProjectsManifest(repoPath) {
14498
- const manifestPath = path40.join(repoPath, "manifest", "projects.yaml");
14499
- const content = await readFileSafe(manifestPath);
14500
- if (!content) {
14501
- return null;
14502
- }
14503
- let raw;
14504
- try {
14505
- raw = YAML8.parse(content);
14506
- } catch (error) {
14507
- throw new Error(`Invalid projects manifest YAML: ${error.message}`);
14508
- }
14509
- return validateManifestShape2(raw);
14510
- }
14511
- async function saveProjectsManifest(repoPath, manifest) {
14512
- validateManifestShape2(manifest);
14513
- const manifestDir = path40.join(repoPath, "manifest");
14514
- const manifestPath = path40.join(manifestDir, "projects.yaml");
14515
- await ensureDir(manifestDir);
14516
- await writeFile(manifestPath, YAML8.stringify(manifest));
14517
- }
14518
- function findProject(manifest, projectId) {
14519
- return manifest.projects.find((candidate) => candidate.id === projectId);
14520
- }
14521
- function listProjectIds(manifest) {
14522
- return manifest.projects.map((project) => project.id);
14523
- }
14524
- function describeProjects(projects) {
14525
- return projects.map((project) => {
14526
- const label = project.name || project.id;
14527
- return project.description ? `${label}: ${project.description}` : label;
14528
- });
14529
- }
14530
- function getProjectOrThrow(manifest, projectId) {
14531
- const project = findProject(manifest, projectId);
14532
- if (!project) {
14533
- throw new Error(`Unknown project "${projectId}". Valid projects: ${listProjectIds(manifest).join(", ")}`);
14534
- }
14535
- return project;
14536
- }
14537
- function resolveProjectResourceNamespaces(input) {
14538
- const resolved = input.activeProjects.map((id) => getProjectOrThrow(input.manifest, id));
14539
- const namespaces = {
14540
- knowledge: [],
14541
- skills: [],
14542
- learnings: []
14543
- };
14544
- for (const type of PROJECT_RESOURCE_TYPES) {
14545
- const seen = /* @__PURE__ */ new Set();
14546
- for (const project of resolved) {
14547
- for (const namespace of project.resources[type]) {
14548
- if (seen.has(namespace)) continue;
14549
- seen.add(namespace);
14550
- namespaces[type].push(namespace);
14551
- }
14552
- }
14553
- }
14554
- return namespaces;
14555
- }
14556
- async function resolveActiveLearningsNamespaces(repoPath, activeProjects) {
14557
- if (activeProjects.length === 0) return [];
14558
- const manifest = await loadProjectsManifest(repoPath);
14559
- if (!manifest) return [];
14560
- try {
14561
- return resolveProjectResourceNamespaces({ manifest, activeProjects }).learnings;
14562
- } catch {
14563
- return [];
14564
- }
14565
- }
14566
- function mergeNamespaces(roleNamespaces, projectNamespaces) {
14567
- const dedupe = (a, b) => {
14568
- const seen = /* @__PURE__ */ new Set();
14569
- const out = [];
14570
- for (const ns of [...a, ...b]) {
14571
- if (seen.has(ns)) continue;
14572
- seen.add(ns);
14573
- out.push(ns);
14574
- }
14575
- return out;
14576
- };
14577
- return {
14578
- knowledge: dedupe(roleNamespaces.knowledge, projectNamespaces.knowledge),
14579
- skills: dedupe(roleNamespaces.skills, projectNamespaces.skills),
14580
- // Roles never contribute learnings; this is effectively the project set.
14581
- learnings: dedupe(roleNamespaces.learnings, projectNamespaces.learnings)
14582
- };
14583
- }
14584
- var PROJECT_RESOURCE_TYPES, ProjectResourceNamespacesSchema, SAFE_ID, ProjectSchema, ProjectsManifestSchema;
14585
- var init_projects = __esm({
14586
- "src/projects.ts"() {
14587
- "use strict";
14588
- init_fs();
14589
- PROJECT_RESOURCE_TYPES = ["knowledge", "skills", "learnings"];
14590
- ProjectResourceNamespacesSchema = z6.object({
14591
- knowledge: z6.array(z6.string().min(1)).default([]),
14592
- skills: z6.array(z6.string().min(1)).default([]),
14593
- learnings: z6.array(z6.string().min(1)).default([])
14594
- });
14595
- SAFE_ID = /^[A-Za-z0-9._-]+$/;
14596
- ProjectSchema = z6.object({
14597
- id: z6.string().min(1).refine((v) => isSafeNamespaceSegment(v), {
14598
- message: "project id must be a single path segment (letters, digits, '.', '_', '-'; no '/', '\\\\', or '..')"
14599
- }),
14600
- name: z6.string().default(""),
14601
- description: z6.string().default(""),
14602
- resources: ProjectResourceNamespacesSchema
14603
- });
14604
- ProjectsManifestSchema = z6.object({
14605
- version: z6.number(),
14606
- // Unlike roles (`.min(1)`), a repo may define zero projects — a team without
14607
- // project partitioning simply has no projects.yaml, and an empty list is valid.
14608
- projects: z6.array(ProjectSchema).default([])
14609
- });
14610
- }
14611
- });
14612
-
14613
15069
  // src/pkg/types.ts
14614
15070
  import { z as z7 } from "zod";
14615
15071
  var PackageScopeSchema, NpmSpecSchema, ClaudeMarketplaceSchema, ClaudePluginSpecSchema, ClaudeEcosystemSchema, PackageSetSchema, PackageManifestSchema, NpmLockEntrySchema, ClaudeMarketplaceLockSchema, ClaudePluginLockSchema, PackageLockSchema;
@@ -14861,6 +15317,7 @@ __export(reports_branch_exports, {
14861
15317
  ensureReportsDir: () => ensureReportsDir,
14862
15318
  ensureReportsWorktree: () => ensureReportsWorktree,
14863
15319
  refreshReportsWorktree: () => refreshReportsWorktree,
15320
+ updateReports: () => updateReports,
14864
15321
  withKnowledgeWorktree: () => withKnowledgeWorktree
14865
15322
  });
14866
15323
  import path42 from "path";
@@ -14868,8 +15325,17 @@ import fse8 from "fs-extra";
14868
15325
  function businessRoot(localConfig) {
14869
15326
  return localConfig.repo.businessRepoRoot ?? path42.dirname(localConfig.repo.localPath);
14870
15327
  }
15328
+ function reportsGitRoot(localConfig) {
15329
+ if (isSelfMode(localConfig)) {
15330
+ return businessRoot(localConfig);
15331
+ }
15332
+ return localConfig.repo.localPath;
15333
+ }
14871
15334
  function reportsWorktreePath(localConfig) {
14872
- return path42.join(localConfig.repo.localPath, REPORTS_WORKTREE_DIRNAME);
15335
+ return getReportsDir(localConfig);
15336
+ }
15337
+ function reportsLockPath(localConfig) {
15338
+ return path42.join(path42.dirname(getReportsDir(localConfig)), REPORTS_LOCK_FILENAME);
14873
15339
  }
14874
15340
  async function remoteBranchExists2(repoRoot) {
14875
15341
  const git = createGit2(repoRoot);
@@ -14881,10 +15347,22 @@ async function remoteBranchExists2(repoRoot) {
14881
15347
  }
14882
15348
  }
14883
15349
  async function ensureReportsWorktree(localConfig, options = {}) {
15350
+ if (!usesReportsBranch(localConfig)) {
15351
+ return getReportsDir(localConfig);
15352
+ }
14884
15353
  const wt = reportsWorktreePath(localConfig);
14885
- const repoRoot = businessRoot(localConfig);
15354
+ const repoRoot = reportsGitRoot(localConfig);
15355
+ if (!isSelfMode(localConfig) && !await isDedicatedRepoRoot(repoRoot)) {
15356
+ throw new Error(
15357
+ `Refusing to create a reports worktree: ${repoRoot} is not a dedicated team-repo clone root`
15358
+ );
15359
+ }
14886
15360
  if (await isGitRepo(wt)) {
14887
- return wt;
15361
+ try {
15362
+ await createGit2(wt).revparse(["--is-inside-work-tree"]);
15363
+ return wt;
15364
+ } catch {
15365
+ }
14888
15366
  }
14889
15367
  if (await pathExists(wt)) {
14890
15368
  await fse8.remove(wt);
@@ -14907,14 +15385,19 @@ async function ensureReportsWorktree(localConfig, options = {}) {
14907
15385
  await git.raw(["worktree", "add", wt, "--track", "-b", REPORTS_BRANCH, `origin/${REPORTS_BRANCH}`]);
14908
15386
  }
14909
15387
  } else {
14910
- await createOrphanWorktree(repoRoot, wt);
14911
- await writeWorktreeGitignore(wt);
14912
- const wtGit = createGit2(wt);
14913
- await wtGit.add([".gitignore"]);
14914
- await commitSkippingHooks(wtGit, "[teamai] Initialize reports branch");
15388
+ const branches = await git.branchLocal();
15389
+ if (branches.all.includes(REPORTS_BRANCH)) {
15390
+ await git.raw(["worktree", "add", wt, REPORTS_BRANCH]);
15391
+ } else {
15392
+ await createOrphanWorktree(repoRoot, wt);
15393
+ await writeWorktreeGitignore(wt);
15394
+ const wtGit = createGit2(wt);
15395
+ await wtGit.add([".gitignore"]);
15396
+ await commitSkippingHooks(wtGit, "[teamai] Initialize reports branch");
15397
+ }
14915
15398
  if (options.pushIfCreated !== false) {
14916
15399
  try {
14917
- await wtGit.push(["-u", "origin", REPORTS_BRANCH]);
15400
+ await createGit2(wt).push(["-u", "origin", REPORTS_BRANCH]);
14918
15401
  } catch (e) {
14919
15402
  log.debug(`[reports] initial push skipped: ${e.message}`);
14920
15403
  }
@@ -14958,45 +15441,48 @@ async function writeWorktreeGitignore(wt) {
14958
15441
  ].join("\n");
14959
15442
  await writeFile(path42.join(wt, ".gitignore"), content);
14960
15443
  }
14961
- async function commitAndPushReports(localConfig, message, files) {
14962
- const lockPath = path42.join(localConfig.repo.localPath, REPORTS_LOCK_FILENAME);
14963
- const locked = await acquireLock(lockPath);
14964
- if (!locked) {
14965
- log.debug("[reports] another reports write is in progress; skipping");
15444
+ async function commitAndPushReportsAt(wt, message, files, options = {}) {
15445
+ const git = createGit2(wt);
15446
+ await git.add(files);
15447
+ const status2 = await git.status();
15448
+ if (status2.staged.length === 0 && !options.pushIfUnchanged) {
15449
+ log.debug("[reports] nothing to commit");
14966
15450
  return false;
14967
15451
  }
14968
- try {
14969
- const wt = await ensureReportsWorktree(localConfig);
14970
- const git = createGit2(wt);
14971
- await git.add(files);
14972
- const status2 = await git.status();
14973
- if (status2.staged.length === 0) {
14974
- log.debug("[reports] nothing to commit");
14975
- return false;
14976
- }
14977
- await commitSkippingHooks(git, message);
14978
- for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
15452
+ if (status2.staged.length > 0) await commitSkippingHooks(git, message);
15453
+ for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
15454
+ try {
15455
+ await git.push(["origin", REPORTS_BRANCH]);
15456
+ return true;
15457
+ } catch (pushErr) {
15458
+ if (attempt === MAX_PUSH_RETRIES) {
15459
+ log.debug(`[reports] push failed after ${attempt} attempts: ${pushErr.message}`);
15460
+ return false;
15461
+ }
14979
15462
  try {
14980
- await git.push(["origin", REPORTS_BRANCH]);
14981
- return true;
14982
- } catch (pushErr) {
14983
- if (attempt === MAX_PUSH_RETRIES) {
14984
- log.debug(`[reports] push failed after ${attempt} attempts: ${pushErr.message}`);
14985
- return false;
14986
- }
15463
+ await git.fetch(["origin", REPORTS_BRANCH]);
15464
+ await git.rebase([`origin/${REPORTS_BRANCH}`]);
15465
+ } catch (rebaseErr) {
15466
+ log.debug(`[reports] rebase failed, retrying: ${rebaseErr.message}`);
14987
15467
  try {
14988
- await git.fetch(["origin", REPORTS_BRANCH]);
14989
- await git.rebase([`origin/${REPORTS_BRANCH}`]);
14990
- } catch (rebaseErr) {
14991
- log.debug(`[reports] rebase failed, retrying: ${rebaseErr.message}`);
14992
- try {
14993
- await git.rebase(["--abort"]);
14994
- } catch {
14995
- }
15468
+ await git.rebase(["--abort"]);
15469
+ } catch {
14996
15470
  }
14997
15471
  }
14998
15472
  }
15473
+ }
15474
+ return false;
15475
+ }
15476
+ async function commitAndPushReports(localConfig, message, files, options = {}) {
15477
+ const lockPath = reportsLockPath(localConfig);
15478
+ const locked = await acquireLock(lockPath);
15479
+ if (!locked) {
15480
+ log.debug("[reports] another reports write is in progress; skipping");
14999
15481
  return false;
15482
+ }
15483
+ try {
15484
+ const wt = await ensureReportsWorktree(localConfig);
15485
+ return await commitAndPushReportsAt(wt, message, files, options);
15000
15486
  } catch (e) {
15001
15487
  log.debug(`[reports] commitAndPushReports failed (non-blocking): ${e.message}`);
15002
15488
  return false;
@@ -15004,18 +15490,167 @@ async function commitAndPushReports(localConfig, message, files) {
15004
15490
  await releaseLock(lockPath);
15005
15491
  }
15006
15492
  }
15493
+ async function updateReports(localConfig, write, options = {}) {
15494
+ if (!usesReportsBranch(localConfig)) {
15495
+ throw new Error("updateReports requires a repo that stores reports on the teamai-reports branch");
15496
+ }
15497
+ const lockPath = reportsLockPath(localConfig);
15498
+ if (!await acquireLock(lockPath)) {
15499
+ log.debug("[reports] another reports write is in progress; skipping");
15500
+ return false;
15501
+ }
15502
+ try {
15503
+ const wt = await ensureReportsWorktree(localConfig);
15504
+ try {
15505
+ await syncReportsWorktree(wt);
15506
+ } catch (e) {
15507
+ log.debug(`[reports] sync before write failed, writing onto the local copy: ${e.message}`);
15508
+ }
15509
+ const change = await write(wt);
15510
+ if (!change || change.files.length === 0) {
15511
+ return false;
15512
+ }
15513
+ return await commitAndPushReportsAt(wt, change.message, change.files, options);
15514
+ } catch (e) {
15515
+ log.debug(`[reports] updateReports failed (non-blocking): ${e.message}`);
15516
+ return false;
15517
+ } finally {
15518
+ await releaseLock(lockPath);
15519
+ }
15520
+ }
15521
+ async function gitPathExists(git, gitPath) {
15522
+ try {
15523
+ const resolved = (await git.raw(["rev-parse", "--git-path", gitPath])).trim();
15524
+ return resolved.length > 0 && await pathExists(resolved);
15525
+ } catch {
15526
+ return false;
15527
+ }
15528
+ }
15529
+ async function rebaseInProgress(git) {
15530
+ return await gitPathExists(git, "rebase-merge") || await gitPathExists(git, "rebase-apply");
15531
+ }
15532
+ async function restoreConflictedFiles(git) {
15533
+ let conflicted = [];
15534
+ try {
15535
+ conflicted = (await git.status()).conflicted ?? [];
15536
+ } catch {
15537
+ return;
15538
+ }
15539
+ if (conflicted.length === 0) {
15540
+ return;
15541
+ }
15542
+ if (await rebaseInProgress(git)) {
15543
+ return;
15544
+ }
15545
+ try {
15546
+ await git.raw(["checkout", "--theirs", "--", ...conflicted]);
15547
+ await git.raw(["add", "--", ...conflicted]);
15548
+ await git.raw(["reset", "HEAD", "--", ...conflicted]);
15549
+ log.debug("[reports] restored uncommitted report files after a stash-apply conflict; using the local copy");
15550
+ } catch (e) {
15551
+ log.debug(`[reports] could not restore stash-apply conflicts: ${e.message}`);
15552
+ try {
15553
+ await git.raw(["reset", "--hard", "HEAD"]);
15554
+ } catch {
15555
+ }
15556
+ }
15557
+ }
15558
+ async function snapshotDirtyTree(git) {
15559
+ try {
15560
+ const sha = (await git.raw(["stash", "create"])).trim();
15561
+ return sha.length > 0 ? sha : null;
15562
+ } catch {
15563
+ return null;
15564
+ }
15565
+ }
15566
+ async function applyDirtySnapshot(git, sha) {
15567
+ try {
15568
+ await git.raw(["stash", "apply", sha]);
15569
+ } catch {
15570
+ }
15571
+ await restoreConflictedFiles(git);
15572
+ }
15573
+ async function syncReportsWorktree(wt) {
15574
+ const git = createGit2(wt);
15575
+ const upstream = `origin/${REPORTS_BRANCH}`;
15576
+ try {
15577
+ await git.fetch(["origin", REPORTS_BRANCH]);
15578
+ } catch (e) {
15579
+ log.debug(`[reports] fetch failed, using the local copy: ${e.message}`);
15580
+ return;
15581
+ }
15582
+ await restoreConflictedFiles(git);
15583
+ const dirty = !(await git.status()).isClean();
15584
+ const ahead = Number.parseInt((await git.raw(["rev-list", "--count", `${upstream}..HEAD`])).trim(), 10);
15585
+ let carried = null;
15586
+ if (dirty && ahead > 0) {
15587
+ carried = await snapshotDirtyTree(git);
15588
+ if (!carried) {
15589
+ log.debug("[reports] uncommitted report files block the refresh; using the local copy");
15590
+ return;
15591
+ }
15592
+ try {
15593
+ await git.raw(["reset", "--hard", "HEAD"]);
15594
+ } catch (e) {
15595
+ log.debug(`[reports] could not clear the worktree for rebase; using the local copy: ${e.message}`);
15596
+ await applyDirtySnapshot(git, carried);
15597
+ return;
15598
+ }
15599
+ }
15600
+ try {
15601
+ if (ahead > 0) {
15602
+ await git.rebase([upstream]);
15603
+ } else {
15604
+ await git.raw(["merge", "--ff-only", upstream]);
15605
+ }
15606
+ } catch (e) {
15607
+ if (ahead > 0) {
15608
+ try {
15609
+ await git.rebase(["--abort"]);
15610
+ } catch {
15611
+ }
15612
+ }
15613
+ if (carried) {
15614
+ await applyDirtySnapshot(git, carried);
15615
+ log.debug(`[reports] uncommitted report files block the refresh; using the local copy: ${e.message}`);
15616
+ return;
15617
+ }
15618
+ if (dirty) {
15619
+ log.debug(`[reports] uncommitted report files block the refresh; using the local copy: ${e.message}`);
15620
+ return;
15621
+ }
15622
+ log.debug(`[reports] dropping ${ahead} unpushed report commit(s) that conflict with ${upstream}: ${e.message}`);
15623
+ await git.raw(["reset", "--hard", upstream]);
15624
+ return;
15625
+ }
15626
+ if (carried) {
15627
+ await applyDirtySnapshot(git, carried);
15628
+ }
15629
+ }
15007
15630
  async function refreshReportsWorktree(localConfig, options = {}) {
15631
+ if (!usesReportsBranch(localConfig)) {
15632
+ return;
15633
+ }
15634
+ const lockPath = reportsLockPath(localConfig);
15635
+ let locked = false;
15008
15636
  try {
15637
+ locked = await acquireLock(lockPath);
15009
15638
  const wt = await ensureReportsWorktree(localConfig, options);
15010
- const git = createGit2(wt);
15011
- await git.fetch(["origin", REPORTS_BRANCH]);
15012
- await git.raw(["reset", "--hard", `origin/${REPORTS_BRANCH}`]);
15639
+ if (!locked) {
15640
+ log.debug("[reports] a reports write is in progress; reading the local copy");
15641
+ return;
15642
+ }
15643
+ await syncReportsWorktree(wt);
15013
15644
  } catch (e) {
15014
15645
  log.debug(`[reports] refresh skipped: ${e.message}`);
15646
+ } finally {
15647
+ if (locked) {
15648
+ await releaseLock(lockPath);
15649
+ }
15015
15650
  }
15016
15651
  }
15017
15652
  async function ensureReportsDir(localConfig) {
15018
- if (localConfig.repo.kind === "self") {
15653
+ if (usesReportsBranch(localConfig)) {
15019
15654
  return ensureReportsWorktree(localConfig);
15020
15655
  }
15021
15656
  return getReportsDir(localConfig);
@@ -15139,10 +15774,11 @@ async function listMembers(options) {
15139
15774
  const projectConfig = await detectProjectConfig();
15140
15775
  const localConfig = projectConfig ?? (await requireInit()).localConfig;
15141
15776
  let repoPath;
15142
- if (localConfig.repo.kind === "self") {
15777
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
15778
+ if (usesReportsBranch2(localConfig)) {
15143
15779
  const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
15144
- await refreshReportsWorktree2(localConfig);
15145
- repoPath = await ensureReportsWorktree2(localConfig);
15780
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
15781
+ repoPath = await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
15146
15782
  } else {
15147
15783
  repoPath = localConfig.repo.localPath;
15148
15784
  await pullRepo(repoPath);
@@ -15346,6 +15982,7 @@ var init_known_agents = __esm({
15346
15982
  // src/init.ts
15347
15983
  var init_exports = {};
15348
15984
  __export(init_exports, {
15985
+ ALL_PROJECTS_SELECTOR: () => ALL_PROJECTS_SELECTOR,
15349
15986
  buildSelfModeGitignore: () => buildSelfModeGitignore,
15350
15987
  init: () => init,
15351
15988
  initHttp: () => initHttp,
@@ -15353,6 +15990,7 @@ __export(init_exports, {
15353
15990
  migrateSelfModeGitignore: () => migrateSelfModeGitignore,
15354
15991
  migrateSelfModeGitignoreContent: () => migrateSelfModeGitignoreContent,
15355
15992
  promptForSelfModeAgents: () => promptForSelfModeAgents,
15993
+ resolveActiveProjects: () => resolveActiveProjects,
15356
15994
  resolveInheritUserScope: () => resolveInheritUserScope,
15357
15995
  resolveInitRepo: () => resolveInitRepo,
15358
15996
  resolveInitScope: () => resolveInitScope,
@@ -15423,6 +16061,10 @@ async function promptForRoleProfile(repoPath, roleFlag) {
15423
16061
  resourceProfileVersion: manifest.version
15424
16062
  };
15425
16063
  }
16064
+ function dedupeIds(ids) {
16065
+ const seen = /* @__PURE__ */ new Set();
16066
+ return ids.filter((id) => seen.has(id) ? false : (seen.add(id), true));
16067
+ }
15426
16068
  async function resolveActiveProjects(repoPath, projectFlag) {
15427
16069
  const requested = (projectFlag ?? "").split(",").map((s) => s.trim()).filter(Boolean);
15428
16070
  if (requested.length === 0) {
@@ -15434,7 +16076,27 @@ async function resolveActiveProjects(repoPath, projectFlag) {
15434
16076
  `--project given but no projects manifest (manifest/projects.yaml) exists in the team repo.`
15435
16077
  );
15436
16078
  }
15437
- const validIds = new Set(listProjectIds(manifest));
16079
+ const declared = listProjectIds(manifest);
16080
+ if (requested.includes(ALL_PROJECTS_SELECTOR)) {
16081
+ if (requested.length > 1) {
16082
+ throw new Error(
16083
+ `--project "${ALL_PROJECTS_SELECTOR}" already covers every declared project; drop the other ids (got: ${requested.join(", ")}).`
16084
+ );
16085
+ }
16086
+ if (declared.length === 0) {
16087
+ log.warn(
16088
+ `--project "${ALL_PROJECTS_SELECTOR}" was given but manifest/projects.yaml declares no projects; nothing was activated.`
16089
+ );
16090
+ return { projects: [] };
16091
+ }
16092
+ if (declared.includes(ALL_PROJECTS_SELECTOR)) {
16093
+ log.warn(
16094
+ `manifest/projects.yaml declares a project with the id "${ALL_PROJECTS_SELECTOR}", which is the reserved --project selector: every project is activated (that one included). To activate only it, run \`teamai projects set ${ALL_PROJECTS_SELECTOR}\`.`
16095
+ );
16096
+ }
16097
+ return { projects: declared };
16098
+ }
16099
+ const validIds = new Set(declared);
15438
16100
  for (const id of requested) {
15439
16101
  if (!validIds.has(id)) {
15440
16102
  throw new Error(
@@ -15442,9 +16104,7 @@ async function resolveActiveProjects(repoPath, projectFlag) {
15442
16104
  );
15443
16105
  }
15444
16106
  }
15445
- const seen = /* @__PURE__ */ new Set();
15446
- const projects = requested.filter((id) => seen.has(id) ? false : (seen.add(id), true));
15447
- return { projects };
16107
+ return { projects: dedupeIds(requested) };
15448
16108
  }
15449
16109
  function resolveInitScope(rawScope, cwd, homeDir) {
15450
16110
  const cwdResolved = resolveRealPath(cwd);
@@ -15834,12 +16494,12 @@ async function initSelfRepo(options) {
15834
16494
  return;
15835
16495
  }
15836
16496
  const provider = getProvider(providerName);
15837
- log.debug(`Detected provider: ${providerName} (from ${remoteUrl})`);
16497
+ log.debug(`Detected provider: ${providerName} (from ${redactGitCredentials(remoteUrl)})`);
15838
16498
  let repoInfo;
15839
16499
  try {
15840
- repoInfo = provider.parseRepoInput(remoteUrl);
16500
+ repoInfo = providerName === "git" ? parseGenericGitExistingRemote(remoteUrl) : provider.parseRepoInput(remoteUrl);
15841
16501
  } catch (e) {
15842
- log.error(`Could not parse the business repo remote "${remoteUrl}": ${e.message}`);
16502
+ log.error(`Could not parse the business repo remote "${redactGitCredentials(remoteUrl)}": ${e.message}`);
15843
16503
  process.exit(1);
15844
16504
  return;
15845
16505
  }
@@ -15962,20 +16622,28 @@ async function initSelfRepo(options) {
15962
16622
  }
15963
16623
  if (!options.dryRun) {
15964
16624
  try {
15965
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
15966
- const wt = await ensureReportsWorktree2(localConfig);
15967
- const memberDir = path45.join(wt, "members");
15968
- await ensureDir(memberDir);
15969
- const memberPath = path45.join(memberDir, `${username}.yaml`);
15970
- const isNewSelfMember = !await pathExists(memberPath);
15971
- const existingSelfMember = await getMemberConfig(wt, username);
15972
- const { config: selfMemberConfig, changed: selfMemberChanged } = mergeMemberConfig(existingSelfMember, {
15973
- username,
15974
- projects: localConfig.projects
16625
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16626
+ let isNewSelfMember = false;
16627
+ let selfMemberChanged = false;
16628
+ const pushed = await updateReports2(localConfig, async (wt) => {
16629
+ const memberDir = path45.join(wt, "members");
16630
+ await ensureDir(memberDir);
16631
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
16632
+ isNewSelfMember = !await pathExists(memberPath);
16633
+ const existingSelfMember = await getMemberConfig(wt, username);
16634
+ const merged = mergeMemberConfig(existingSelfMember, {
16635
+ username,
16636
+ projects: localConfig.projects
16637
+ });
16638
+ selfMemberChanged = merged.changed;
16639
+ if (!merged.changed) return null;
16640
+ await writeFile(memberPath, YAML11.stringify(merged.config));
16641
+ return {
16642
+ files: ["members/"],
16643
+ message: isNewSelfMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`
16644
+ };
15975
16645
  });
15976
16646
  if (selfMemberChanged) {
15977
- await writeFile(memberPath, YAML11.stringify(selfMemberConfig));
15978
- const pushed = await commitAndPushReports2(localConfig, isNewSelfMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`, ["members/"]);
15979
16647
  if (pushed) {
15980
16648
  log.success(isNewSelfMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
15981
16649
  } else {
@@ -16150,6 +16818,17 @@ async function init(options) {
16150
16818
  }
16151
16819
  } else {
16152
16820
  log.info(`Repo already exists at ${localPath}, using existing clone`);
16821
+ try {
16822
+ const pullResult = await pullRepoFastForward(localPath);
16823
+ if (pullResult !== "already up to date") {
16824
+ log.info(`Refreshed existing clone (${pullResult})`);
16825
+ }
16826
+ } catch (e) {
16827
+ log.error(
16828
+ `Failed to refresh existing clone at ${localPath}: ${e.message}. The local clone was left unchanged. Fix network/auth or resolve divergence (commit/stash local edits), or remove ${localPath} manually to start fresh.`
16829
+ );
16830
+ process.exit(1);
16831
+ }
16153
16832
  }
16154
16833
  } else {
16155
16834
  log.warn(`Existing ${localPath} is not a git repository, re-cloning`);
@@ -16242,6 +16921,7 @@ async function init(options) {
16242
16921
  const emailDomain = provider.getDefaultEmailDomain() ?? void 0;
16243
16922
  await configureGitUser(localPath, username, username, void 0, emailDomain);
16244
16923
  const teamConfig = await loadTeamConfig(localPath);
16924
+ const createdSkeleton = !teamConfig;
16245
16925
  if (!teamConfig) {
16246
16926
  log.warn("teamai.yaml not found in repo. Creating default config...");
16247
16927
  const defaultConfig = YAML11.stringify({
@@ -16271,33 +16951,67 @@ async function init(options) {
16271
16951
  log.error(error.message);
16272
16952
  process.exit(1);
16273
16953
  }
16274
- const memberPath = path45.join(localPath, "members", `${username}.yaml`);
16275
- const isNewMember = !await pathExists(memberPath);
16276
- const existingMember = await getMemberConfig(localPath, username);
16277
- const { config: memberConfig, changed: memberChanged } = mergeMemberConfig(existingMember, {
16954
+ const reportsConfig = {
16955
+ repo: { localPath, remote: repoInfo.httpsUrl },
16278
16956
  username,
16279
- projects: resolvedProjects
16280
- });
16281
- if (memberChanged) {
16282
- await writeFile(memberPath, YAML11.stringify(memberConfig));
16283
- log.success(isNewMember ? `Registered as team member: ${username}` : `Updated member roster: ${username}${memberConfig.projects ? ` (projects: ${memberConfig.projects.join(", ")})` : ""}`);
16284
- if (!options.dryRun) {
16285
- try {
16286
- await pushRepoDirectly(localPath, isNewMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`, [
16287
- "members/",
16288
- "teamai.yaml",
16289
- "skills/.gitkeep",
16290
- "rules/.gitkeep",
16291
- "docs/.gitkeep",
16292
- "env/.gitkeep"
16293
- ]);
16294
- log.success("Member registration pushed to team repo");
16295
- } catch (e) {
16296
- log.warn(`Push failed (you can push manually later): ${e.message}`);
16957
+ scope,
16958
+ projectRoot,
16959
+ additionalRoles: []
16960
+ };
16961
+ if (createdSkeleton && !options.dryRun) {
16962
+ try {
16963
+ await pushRepoDirectly(localPath, "[teamai] Initialize team repo skeleton", [
16964
+ "teamai.yaml",
16965
+ "skills/.gitkeep",
16966
+ "rules/.gitkeep",
16967
+ "docs/.gitkeep",
16968
+ "env/.gitkeep",
16969
+ "members/.gitkeep"
16970
+ ]);
16971
+ } catch (e) {
16972
+ log.warn(`Push failed (you can push manually later): ${e.message}`);
16973
+ }
16974
+ }
16975
+ let isNewMember = true;
16976
+ if (!options.dryRun) {
16977
+ try {
16978
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16979
+ let memberChanged = false;
16980
+ let memberProjects;
16981
+ const pushed = await updateReports2(reportsConfig, async (wt) => {
16982
+ const memberDir = path45.join(wt, "members");
16983
+ await ensureDir(memberDir);
16984
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
16985
+ isNewMember = !await pathExists(memberPath);
16986
+ const existingMember = await getMemberConfig(wt, username);
16987
+ const merged = mergeMemberConfig(existingMember, {
16988
+ username,
16989
+ projects: resolvedProjects
16990
+ });
16991
+ memberChanged = merged.changed;
16992
+ memberProjects = merged.config.projects;
16993
+ if (!merged.changed) return null;
16994
+ await writeFile(memberPath, YAML11.stringify(merged.config));
16995
+ return {
16996
+ files: ["members/"],
16997
+ message: isNewMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`
16998
+ };
16999
+ });
17000
+ if (memberChanged) {
17001
+ log.success(isNewMember ? `Registered as team member: ${username}` : `Updated member roster: ${username}${memberProjects ? ` (projects: ${memberProjects.join(", ")})` : ""}`);
17002
+ if (pushed) {
17003
+ log.success(isNewMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
17004
+ } else {
17005
+ log.warn("Member registration could not be pushed (no write access?). You are still set up locally.");
17006
+ }
17007
+ } else if (!isNewMember) {
17008
+ log.info(`Member ${username} already registered`);
16297
17009
  }
17010
+ } catch (e) {
17011
+ log.warn(`Member registration skipped (non-blocking): ${e.message}`);
16298
17012
  }
16299
17013
  } else {
16300
- log.info(`Member ${username} already registered`);
17014
+ log.info(`[dry-run] Would register member ${username} on the teamai-reports branch`);
16301
17015
  }
16302
17016
  const currentConfig = await loadTeamConfig(localPath);
16303
17017
  const hasReviewers = currentConfig?.reviewers && currentConfig.reviewers.length > 0;
@@ -16417,6 +17131,7 @@ async function init(options) {
16417
17131
  log.info("Run `teamai status` to check current config.");
16418
17132
  closePrompt();
16419
17133
  }
17134
+ var ALL_PROJECTS_SELECTOR;
16420
17135
  var init_init = __esm({
16421
17136
  "src/init.ts"() {
16422
17137
  "use strict";
@@ -16425,6 +17140,7 @@ var init_init = __esm({
16425
17140
  init_git2();
16426
17141
  init_git2();
16427
17142
  init_providers();
17143
+ init_repo_url5();
16428
17144
  init_fs();
16429
17145
  init_logger();
16430
17146
  init_types();
@@ -16434,6 +17150,7 @@ var init_init = __esm({
16434
17150
  init_members();
16435
17151
  init_prompt();
16436
17152
  init_known_agents();
17153
+ ALL_PROJECTS_SELECTOR = "all";
16437
17154
  }
16438
17155
  });
16439
17156
 
@@ -16756,13 +17473,19 @@ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConf
16756
17473
  }
16757
17474
  } : teamConfig;
16758
17475
  const pushableTypes = ["skills", "rules", "env", "agents"];
16759
- const allItems = [];
17476
+ const fullScan = [];
16760
17477
  for (const type of pushableTypes) {
16761
17478
  const handler = getHandler(type);
16762
17479
  const items = await handler.scanLocalForPush(scanTeamConfig, localConfig);
16763
- allItems.push(...items);
17480
+ fullScan.push(...items);
16764
17481
  }
16765
- const fullScan = [...allItems];
17482
+ const allItems = fullScan.filter((item) => {
17483
+ if (item.type === "agents" && "skipReason" in item && typeof item.skipReason === "string" && item.skipReason) {
17484
+ log.warn(`[agents] Skipped ${item.name}: ${item.skipReason}`);
17485
+ return false;
17486
+ }
17487
+ return true;
17488
+ });
16766
17489
  spin.stop();
16767
17490
  if (options.skill) {
16768
17491
  const skillBasename = path46.basename(
@@ -17148,6 +17871,7 @@ __export(git_exports, {
17148
17871
  listWorktrees: () => listWorktrees,
17149
17872
  normalizeRepoUrlForCompare: () => normalizeRepoUrlForCompare,
17150
17873
  pullRepo: () => pullRepo,
17874
+ pullRepoFastForward: () => pullRepoFastForward,
17151
17875
  pushLearningToOrigin: () => pushLearningToOrigin,
17152
17876
  pushRepoBranch: () => pushRepoBranch,
17153
17877
  pushRepoDirectly: () => pushRepoDirectly,
@@ -17286,6 +18010,14 @@ async function pullRepo(localPath) {
17286
18010
  return "reset to origin (diverged)";
17287
18011
  }
17288
18012
  }
18013
+ async function pullRepoFastForward(localPath) {
18014
+ const git = createGit2(localPath);
18015
+ const result = await git.pull(["--ff-only"]);
18016
+ if (result.summary.changes === 0 && result.summary.insertions === 0 && result.summary.deletions === 0) {
18017
+ return "already up to date";
18018
+ }
18019
+ return `${result.summary.changes} file(s) changed`;
18020
+ }
17289
18021
  async function getDefaultBranch(localPath) {
17290
18022
  const cached = defaultBranchCache.get(localPath);
17291
18023
  if (cached) return cached;
@@ -17861,19 +18593,19 @@ async function bootstrapSelfRepo(dir, opts) {
17861
18593
  log.debug(`[bootstrap] hook injection failed (non-blocking): ${e.message}`);
17862
18594
  }
17863
18595
  try {
17864
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
17865
- const wt = await ensureReportsWorktree2(localConfig);
17866
- const memberDir = path49.join(wt, "members");
17867
- await ensureDir(memberDir);
17868
- const memberPath = path49.join(memberDir, `${username}.yaml`);
17869
- if (!await pathExists(memberPath)) {
18596
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
18597
+ await updateReports2(localConfig, async (wt) => {
18598
+ const memberDir = path49.join(wt, "members");
18599
+ await ensureDir(memberDir);
18600
+ const memberPath = path49.join(memberDir, `${username}.yaml`);
18601
+ if (await pathExists(memberPath)) return null;
17870
18602
  await writeFile(memberPath, YAML14.stringify({
17871
18603
  username,
17872
18604
  displayName: username,
17873
18605
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
17874
18606
  }));
17875
- await commitAndPushReports2(localConfig, `[teamai] Register member: ${username}`, ["members/"]);
17876
- }
18607
+ return { files: ["members/"], message: `[teamai] Register member: ${username}` };
18608
+ });
17877
18609
  } catch (e) {
17878
18610
  log.debug(`[bootstrap] member registration skipped (non-blocking): ${e.message}`);
17879
18611
  }
@@ -18141,68 +18873,6 @@ var init_config = __esm({
18141
18873
  }
18142
18874
  });
18143
18875
 
18144
- // src/utils/exec.ts
18145
- import spawn from "cross-spawn";
18146
- async function probeBinary(binary, args = ["--version"], executor = execCommand) {
18147
- try {
18148
- const result = await executor(binary, args, { timeoutMs: 5e3 });
18149
- if (result.code !== 0) return "";
18150
- return (result.stdout || result.stderr).trim();
18151
- } catch {
18152
- return "";
18153
- }
18154
- }
18155
- function formatCommand(command, args) {
18156
- const quote = (value) => /^[a-zA-Z0-9_./:@=+-]+$/.test(value) ? value : JSON.stringify(value);
18157
- return [command, ...args.map(quote)].join(" ");
18158
- }
18159
- var execCommand;
18160
- var init_exec = __esm({
18161
- "src/utils/exec.ts"() {
18162
- "use strict";
18163
- execCommand = (command, args, options = {}) => new Promise((resolve, reject) => {
18164
- const child = spawn(command, args, {
18165
- cwd: options.cwd,
18166
- env: options.env,
18167
- stdio: ["ignore", "pipe", "pipe"],
18168
- shell: false
18169
- });
18170
- let stdout = "";
18171
- let stderr = "";
18172
- let settled = false;
18173
- const timeoutMs = options.timeoutMs ?? 12e4;
18174
- const finish = (callback) => {
18175
- if (settled) return;
18176
- settled = true;
18177
- clearTimeout(timer);
18178
- callback();
18179
- };
18180
- child.stdout?.on("data", (chunk) => {
18181
- const text = chunk.toString();
18182
- stdout += text;
18183
- if (options.stream) process.stdout.write(text);
18184
- });
18185
- child.stderr?.on("data", (chunk) => {
18186
- const text = chunk.toString();
18187
- stderr += text;
18188
- if (options.stream) process.stderr.write(text);
18189
- });
18190
- const timer = setTimeout(() => {
18191
- child.kill();
18192
- finish(() => reject(new Error(
18193
- `Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`
18194
- )));
18195
- }, timeoutMs);
18196
- child.on("error", (error) => finish(() => reject(error)));
18197
- child.on("close", (code) => finish(() => resolve({
18198
- stdout,
18199
- stderr,
18200
- code: code ?? 1
18201
- })));
18202
- });
18203
- }
18204
- });
18205
-
18206
18876
  // src/pkg/adapters/base.ts
18207
18877
  var PackageAdapter;
18208
18878
  var init_base2 = __esm({
@@ -19208,9 +19878,9 @@ async function verifyStaging(legacyDir, staging) {
19208
19878
  throw new Error("migration verify: team-repo/.git missing after copy (clone would be broken)");
19209
19879
  }
19210
19880
  try {
19211
- const { execFile: execFile5 } = await import("child_process");
19881
+ const { execFile: execFile4 } = await import("child_process");
19212
19882
  const { promisify: promisify4 } = await import("util");
19213
- await promisify4(execFile5)("git", ["rev-parse", "HEAD"], { cwd: stagedRepo });
19883
+ await promisify4(execFile4)("git", ["rev-parse", "HEAD"], { cwd: stagedRepo });
19214
19884
  } catch (e) {
19215
19885
  throw new Error(
19216
19886
  `migration verify: the staged team-repo clone is not a usable git repository (${e.message})`
@@ -19595,6 +20265,7 @@ var init_tokenizer = __esm({
19595
20265
  // src/votes.ts
19596
20266
  var votes_exports = {};
19597
20267
  __export(votes_exports, {
20268
+ hasPendingVoteDeltas: () => hasPendingVoteDeltas,
19598
20269
  incrementRecalled: () => incrementRecalled,
19599
20270
  incrementUpvoted: () => incrementUpvoted,
19600
20271
  loadUserVotes: () => loadUserVotes,
@@ -19700,9 +20371,19 @@ function mergeDeltas(local, remote) {
19700
20371
  votes[docId].last_upvoted_at = localEntry.last_upvoted_at;
19701
20372
  }
19702
20373
  }
20374
+ if (votes[docId].recalled_count === 0) {
20375
+ votes[docId].last_recalled_at = "";
20376
+ }
20377
+ if (votes[docId].upvoted_count === 0) {
20378
+ delete votes[docId].last_upvoted_at;
20379
+ }
19703
20380
  }
19704
20381
  return { version: 2, votes, deltas: {} };
19705
20382
  }
20383
+ async function hasPendingVoteDeltas(localVotesDir, username) {
20384
+ const local = await loadUserVotes(path57.join(localVotesDir, `${username}.yaml`));
20385
+ return Object.keys(local.deltas).length > 0;
20386
+ }
19706
20387
  async function syncVotesToTeam(repoPath, username, localVotesDir) {
19707
20388
  const localVotePath = path57.join(localVotesDir, `${username}.yaml`);
19708
20389
  const remoteVotePath = path57.join(repoPath, "votes", `${username}.yaml`);
@@ -19739,11 +20420,16 @@ async function recallFeedback(opts) {
19739
20420
  return;
19740
20421
  }
19741
20422
  const existingDelta = data.deltas[opts.negative] ?? { recalled_delta: 0, upvoted_delta: 0 };
20423
+ const decrementedCount = entry.upvoted_count - 1;
20424
+ const updatedEntry = { ...entry, upvoted_count: decrementedCount };
20425
+ if (decrementedCount === 0) {
20426
+ delete updatedEntry.last_upvoted_at;
20427
+ }
19742
20428
  const updated = {
19743
20429
  ...data,
19744
20430
  votes: {
19745
20431
  ...data.votes,
19746
- [opts.negative]: { ...entry, upvoted_count: entry.upvoted_count - 1 }
20432
+ [opts.negative]: updatedEntry
19747
20433
  },
19748
20434
  deltas: {
19749
20435
  ...data.deltas,
@@ -21473,10 +22159,11 @@ async function generateDigest(options) {
21473
22159
  const localConfig = projectConfig ?? (await requireInit()).localConfig;
21474
22160
  const repoPath = localConfig.repo.localPath;
21475
22161
  let reportsRoot = repoPath;
21476
- if (localConfig.repo.kind === "self") {
22162
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
22163
+ if (usesReportsBranch2(localConfig)) {
21477
22164
  const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
21478
- await refreshReportsWorktree2(localConfig);
21479
- reportsRoot = await ensureReportsWorktree2(localConfig);
22165
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
22166
+ reportsRoot = await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
21480
22167
  }
21481
22168
  const teamStats = await loadTeamStats(reportsRoot);
21482
22169
  if (teamStats.length === 0) {
@@ -21629,9 +22316,10 @@ async function loadReportedStats() {
21629
22316
  const config = await detectProjectConfig() ?? await loadLocalConfig();
21630
22317
  if (!config) return null;
21631
22318
  let statsRoot = config.repo.localPath;
21632
- if (config.repo.kind === "self") {
22319
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
22320
+ if (usesReportsBranch2(config)) {
21633
22321
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
21634
- statsRoot = await ensureReportsWorktree2(config);
22322
+ statsRoot = await ensureReportsWorktree2(config, { pushIfCreated: false });
21635
22323
  }
21636
22324
  const statsPath = path62.join(statsRoot, "stats", `${config.username}.yaml`);
21637
22325
  const content = await readFileSafe(statsPath);
@@ -22105,8 +22793,9 @@ function filterEventsByScope(events, opts) {
22105
22793
  return events;
22106
22794
  }
22107
22795
  async function reportUsageToTeam(repoPath, username, options) {
22108
- const selfConfig = options?.selfConfig;
22109
- const selfMode = selfConfig?.repo.kind === "self";
22796
+ const reportsConfig = options?.selfConfig;
22797
+ const useReportsBranch = !!reportsConfig && usesReportsBranch(reportsConfig);
22798
+ let restoreStats;
22110
22799
  try {
22111
22800
  const events = await readUsageEvents();
22112
22801
  const filesToPush = [];
@@ -22135,20 +22824,77 @@ async function reportUsageToTeam(repoPath, username, options) {
22135
22824
  const hasInterventions = hasInterventionDelta(interventionDelta);
22136
22825
  const hasPromptTokens = hasPromptTokenDelta(promptTokenDelta);
22137
22826
  const hasDaily = hasDailyDelta(dailyDelta);
22138
- let writeRoot = repoPath;
22139
- if (selfMode && selfConfig) {
22140
- const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22141
- writeRoot = await ensureReportsWorktree2(selfConfig);
22827
+ const hasStats = hasUsage || hasInterventions || hasPromptTokens || hasDaily;
22828
+ const commitMsg = hasUsage ? `[teamai] Update usage stats for ${username}` : hasInterventions || hasPromptTokens || hasDaily ? `[teamai] Update session stats for ${username}` : `[teamai] Update votes for ${username}`;
22829
+ const writeReportFiles = async (writeRoot) => {
22830
+ if (hasStats) {
22831
+ const statsDir = path64.join(writeRoot, "stats");
22832
+ await ensureDir(statsDir);
22833
+ const statsPath = path64.join(statsDir, `${username}.yaml`);
22834
+ const existing = await readExistingStats(statsPath);
22835
+ if (useReportsBranch) {
22836
+ const previousContent = await readFileSafe(statsPath);
22837
+ restoreStats = () => writeFile(statsPath, previousContent ?? "");
22838
+ }
22839
+ const newStats = hasUsage ? aggregateUsage(events) : [];
22840
+ const merged = mergeStats(existing, username, newStats);
22841
+ if (hasInterventions) {
22842
+ merged.interventions = mergeInterventionStats(existing?.interventions, interventionDelta);
22843
+ }
22844
+ if (hasPromptTokens) {
22845
+ const pt = mergePromptTokenStats(existing?.prompts, existing?.tokens, promptTokenDelta);
22846
+ merged.prompts = pt.prompts;
22847
+ merged.tokens = pt.tokens;
22848
+ }
22849
+ if (hasDaily) {
22850
+ merged.daily = mergeDailyStats(existing?.daily, dailyDelta);
22851
+ }
22852
+ await writeFile(statsPath, YAML21.stringify(merged));
22853
+ filesToPush.push(`stats/${username}.yaml`);
22854
+ }
22855
+ try {
22856
+ if (await pathExists(getUserVotesDir())) {
22857
+ const { syncVotesToTeam: syncVotesToTeam2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
22858
+ const synced = await syncVotesToTeam2(writeRoot, username, getUserVotesDir());
22859
+ if (synced) {
22860
+ filesToPush.push(`votes/${username}.yaml`);
22861
+ }
22862
+ }
22863
+ } catch (e) {
22864
+ log.error(`Vote staging skipped: ${e.message}`);
22865
+ }
22866
+ };
22867
+ if (useReportsBranch && reportsConfig) {
22868
+ let hasVotes = false;
22869
+ if (!hasStats && await pathExists(getUserVotesDir())) {
22870
+ const { hasPendingVoteDeltas: hasPendingVoteDeltas2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
22871
+ hasVotes = await hasPendingVoteDeltas2(getUserVotesDir(), username);
22872
+ }
22873
+ if (!hasStats && !hasVotes) {
22874
+ log.debug("No usage events or votes to report");
22875
+ return true;
22876
+ }
22877
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22878
+ const pushed = await updateReports2(reportsConfig, async (wt) => {
22879
+ filesToPush.length = 0;
22880
+ await writeReportFiles(wt);
22881
+ return filesToPush.length > 0 ? { files: [...filesToPush], message: commitMsg } : null;
22882
+ }, { pushIfUnchanged: true });
22883
+ if (!pushed) {
22884
+ log.debug("Auto-report push was not confirmed; keeping local report data");
22885
+ await restoreStats?.();
22886
+ return false;
22887
+ }
22142
22888
  } else {
22143
22889
  const git = createGit2(repoPath);
22144
22890
  if (!await isDedicatedRepoRoot(repoPath)) {
22145
22891
  log.debug(`Skipping report: ${repoPath} is not a dedicated team-repo root (safety guard)`);
22146
- return;
22892
+ return false;
22147
22893
  }
22148
22894
  const { isImportInProgress: isImportInProgress2 } = await Promise.resolve().then(() => (init_import_lock(), import_lock_exports));
22149
22895
  if (await isImportInProgress2(repoPath)) {
22150
22896
  log.debug(`Skipping report: import in progress for ${repoPath} (would reset uncommitted artifacts)`);
22151
- return;
22897
+ return false;
22152
22898
  }
22153
22899
  const yamlPath = path64.join(repoPath, "teamai.yaml");
22154
22900
  const workingContent = await readFileSafe(yamlPath);
@@ -22162,58 +22908,14 @@ async function reportUsageToTeam(repoPath, username, options) {
22162
22908
  await writeFile(yamlPath, pendingTeamConfig);
22163
22909
  }
22164
22910
  }
22165
- }
22166
- if (hasUsage || hasInterventions || hasPromptTokens || hasDaily) {
22167
- const statsDir = path64.join(writeRoot, "stats");
22168
- await ensureDir(statsDir);
22169
- const statsPath = path64.join(statsDir, `${username}.yaml`);
22170
- const existing = await readExistingStats(statsPath);
22171
- const newStats = hasUsage ? aggregateUsage(events) : [];
22172
- const merged = mergeStats(existing, username, newStats);
22173
- if (hasInterventions) {
22174
- merged.interventions = mergeInterventionStats(existing?.interventions, interventionDelta);
22175
- }
22176
- if (hasPromptTokens) {
22177
- const pt = mergePromptTokenStats(existing?.prompts, existing?.tokens, promptTokenDelta);
22178
- merged.prompts = pt.prompts;
22179
- merged.tokens = pt.tokens;
22180
- }
22181
- if (hasDaily) {
22182
- merged.daily = mergeDailyStats(existing?.daily, dailyDelta);
22183
- }
22184
- await writeFile(statsPath, YAML21.stringify(merged));
22185
- filesToPush.push(`stats/${username}.yaml`);
22186
- }
22187
- try {
22188
- if (await pathExists(getUserVotesDir())) {
22189
- const { syncVotesToTeam: syncVotesToTeam2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
22190
- const synced = await syncVotesToTeam2(writeRoot, username, getUserVotesDir());
22191
- if (synced) {
22192
- filesToPush.push(`votes/${username}.yaml`);
22193
- }
22911
+ await writeReportFiles(repoPath);
22912
+ if (filesToPush.length === 0) {
22913
+ log.debug("No usage events or votes to report");
22914
+ return true;
22194
22915
  }
22195
- } catch (e) {
22196
- log.error(`Vote staging skipped: ${e.message}`);
22197
- }
22198
- if (filesToPush.length === 0) {
22199
- log.debug("No usage events or votes to report");
22200
- return;
22201
- }
22202
- const commitMsg = hasUsage ? `[teamai] Update usage stats for ${username}` : hasInterventions || hasPromptTokens || hasDaily ? `[teamai] Update session stats for ${username}` : `[teamai] Update votes for ${username}`;
22203
- if (selfMode && selfConfig) {
22204
- const { commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22205
- await withTimeout(
22206
- commitAndPushReports2(selfConfig, commitMsg, filesToPush),
22207
- 5e3,
22208
- "Auto-report timeout (5s)"
22209
- );
22210
- } else {
22211
- await withTimeout(
22212
- pushRepoDirectly(repoPath, commitMsg, filesToPush),
22213
- 5e3,
22214
- "Auto-report timeout (5s)"
22215
- );
22916
+ await pushRepoDirectly(repoPath, commitMsg, filesToPush);
22216
22917
  }
22918
+ restoreStats = void 0;
22217
22919
  if (hasUsage && !options?.skipTruncate) {
22218
22920
  await truncateUsageAfterReport(events.length);
22219
22921
  log.debug(`Reported ${events.length} usage events to team repo`);
@@ -22238,8 +22940,15 @@ async function reportUsageToTeam(repoPath, username, options) {
22238
22940
  if (!hasUsage && !hasInterventions && !hasPromptTokens && !hasDaily) {
22239
22941
  log.debug("Pushed pending votes to team repo");
22240
22942
  }
22943
+ return true;
22241
22944
  } catch (e) {
22945
+ try {
22946
+ await restoreStats?.();
22947
+ } catch (restoreError) {
22948
+ log.error(`Could not restore report stats after failure: ${restoreError.message}`);
22949
+ }
22242
22950
  log.error(`Auto-report skipped: ${e.message}`);
22951
+ return false;
22243
22952
  }
22244
22953
  }
22245
22954
  var init_team_push = __esm({
@@ -22249,7 +22958,6 @@ var init_team_push = __esm({
22249
22958
  init_stats();
22250
22959
  init_dashboard_collector();
22251
22960
  init_git2();
22252
- init_async();
22253
22961
  init_fs();
22254
22962
  init_logger();
22255
22963
  init_types();
@@ -22427,6 +23135,7 @@ __export(pull_exports, {
22427
23135
  compileClaudemd: () => compileClaudemd,
22428
23136
  compileCulture: () => compileCulture,
22429
23137
  compileRecallRulesBlock: () => compileRecallRulesBlock,
23138
+ filterAgentsByNamespaces: () => filterAgentsByNamespaces,
22430
23139
  filterRulesByKnowledgeNamespaces: () => filterRulesByKnowledgeNamespaces,
22431
23140
  injectRecallBlockIntoTools: () => injectRecallBlockIntoTools,
22432
23141
  pull: () => pull,
@@ -22504,58 +23213,9 @@ async function usageReportDisabled(repoPath) {
22504
23213
  return (await loadTeamConfig(repoPath))?.usageReport === false;
22505
23214
  }
22506
23215
  async function buildRolePullContext(localConfig) {
22507
- const activeProjects = localConfig.projects ?? [];
22508
- const hasRole = !!localConfig.primaryRole;
22509
- const hasProjects = activeProjects.length > 0;
22510
- const projectsManifest = await loadProjectsManifest(localConfig.repo.localPath);
22511
- const teamHasProjects = !!projectsManifest && projectsManifest.projects.length > 0;
22512
- if (!hasRole && !hasProjects && !teamHasProjects) return null;
22513
- let roleNamespaces = { knowledge: [], skills: [], learnings: [] };
22514
- let allRoleSkillNamespaces = /* @__PURE__ */ new Set();
22515
- if (hasRole) {
22516
- let rolesManifest;
22517
- try {
22518
- rolesManifest = await loadRolesManifest(localConfig.repo.localPath);
22519
- } catch {
22520
- log.warn("Could not load roles manifest. Skipping role-based filtering.");
22521
- rolesManifest = null;
22522
- }
22523
- if (rolesManifest) {
22524
- try {
22525
- roleNamespaces = resolveRoleResourceNamespaces({
22526
- manifest: rolesManifest,
22527
- primaryRole: localConfig.primaryRole,
22528
- additionalRoles: localConfig.additionalRoles ?? []
22529
- });
22530
- allRoleSkillNamespaces = new Set(rolesManifest.roles.flatMap((role) => role.resources.skills));
22531
- } catch {
22532
- log.warn(`Role "${localConfig.primaryRole}" not found in manifest. Falling back to unfiltered sync.`);
22533
- log.warn("Run `teamai roles set <role>` to pick a valid role.");
22534
- if (!hasProjects && !teamHasProjects) return null;
22535
- }
22536
- } else if (!hasProjects && !teamHasProjects) {
22537
- return null;
22538
- }
22539
- }
22540
- let projectNamespaces = { knowledge: [], skills: [], learnings: [] };
22541
- let allProjectSkillNamespaces = /* @__PURE__ */ new Set();
22542
- if (projectsManifest) {
22543
- allProjectSkillNamespaces = new Set(projectsManifest.projects.flatMap((p) => p.resources.skills));
22544
- if (hasProjects) {
22545
- try {
22546
- projectNamespaces = resolveProjectResourceNamespaces({
22547
- manifest: projectsManifest,
22548
- activeProjects
22549
- });
22550
- } catch (e) {
22551
- log.warn(`${e.message} Falling back to role-only filtering.`);
22552
- }
22553
- }
22554
- } else if (hasProjects) {
22555
- log.warn("Active projects configured but no projects manifest found. Skipping project-based filtering.");
22556
- }
22557
- const activeNamespaces = mergeNamespaces(roleNamespaces, projectNamespaces);
22558
- const allSkillNamespaces = /* @__PURE__ */ new Set([...allRoleSkillNamespaces, ...allProjectSkillNamespaces]);
23216
+ const resolved = await resolveResourceNamespaces(localConfig);
23217
+ if (!resolved) return null;
23218
+ const { activeNamespaces, allSkillNamespaces } = resolved;
22559
23219
  const inactiveSkillNamespaces = [...allSkillNamespaces].filter((namespace) => !activeNamespaces.skills.includes(namespace));
22560
23220
  const activeSkillNames = /* @__PURE__ */ new Set();
22561
23221
  const inactiveSkillNames = /* @__PURE__ */ new Set();
@@ -22588,6 +23248,20 @@ function filterRulesByKnowledgeNamespaces(rules, knowledgeNamespaces) {
22588
23248
  return knowledgeNamespaces.includes(namespace);
22589
23249
  });
22590
23250
  }
23251
+ function filterAgentsByNamespaces(agents, agentNamespaces) {
23252
+ const kept = agentNamespaces ? agents.filter((agent) => !agent.namespace || agentNamespaces.includes(agent.namespace)) : agents;
23253
+ const seen = /* @__PURE__ */ new Map();
23254
+ for (const agent of kept) {
23255
+ const existing = seen.get(agent.name);
23256
+ if (existing) {
23257
+ throw new Error(
23258
+ `Duplicate agent "${agent.name}" found in active namespaces "${existing.namespace ?? "(root)"}" and "${agent.namespace ?? "(root)"}"`
23259
+ );
23260
+ }
23261
+ seen.set(agent.name, agent);
23262
+ }
23263
+ return kept;
23264
+ }
22591
23265
  async function scanRoleAwareSkills(localConfig, namespaces) {
22592
23266
  const items = /* @__PURE__ */ new Map();
22593
23267
  for (const namespace of namespaces.skills) {
@@ -22689,15 +23363,46 @@ async function getInstalledResourceTargets(teamConfig, localConfig) {
22689
23363
  }
22690
23364
  return targets.sort();
22691
23365
  }
23366
+ function tombstoneExtensions(type, tool) {
23367
+ if (type === "rules") return [.../* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"])];
23368
+ if (type === "agents") return AGENT_FILE_EXTENSIONS;
23369
+ return [""];
23370
+ }
23371
+ async function cleanupTombstonedResources(freshConfig, localConfig, scopeLabel) {
23372
+ const tombstoneTypes = [
23373
+ { type: "rules", toolPathField: "rules" },
23374
+ { type: "skills", toolPathField: "skills" },
23375
+ { type: "agents", toolPathField: "agents" }
23376
+ ];
23377
+ const baseDir = resolveBaseDir(localConfig);
23378
+ for (const { type, toolPathField } of tombstoneTypes) {
23379
+ const handler = getHandler(type);
23380
+ const tombstones = await handler.readTombstones(localConfig);
23381
+ if (tombstones.size === 0) continue;
23382
+ for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
23383
+ const dir = toolPath[toolPathField];
23384
+ if (!dir) continue;
23385
+ if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
23386
+ if (isAgentExcluded(localConfig, tool)) continue;
23387
+ for (const name of tombstones) {
23388
+ for (const extension of tombstoneExtensions(type, tool)) {
23389
+ const localPath = path66.join(baseDir, dir, `${name}${extension}`);
23390
+ if (!await pathExists(localPath)) continue;
23391
+ if (type === "skills" && await hasVcsMetadataRecursive(localPath)) {
23392
+ log.warn(`[${scopeLabel}] Kept tombstoned skill "${name}" (${tool}): it has local VCS metadata (.git) that may hold unpushed history. Back it up, then delete it manually.`);
23393
+ continue;
23394
+ }
23395
+ await remove(localPath);
23396
+ log.debug(`[${scopeLabel}] Cleaned up tombstoned ${type} ${name} from ${dir}`);
23397
+ }
23398
+ }
23399
+ }
23400
+ }
23401
+ }
22692
23402
  async function pullForScope(localConfig, options, policy = {}) {
22693
23403
  const scopeLabel = localConfig.scope;
22694
23404
  const revisionField = policy.revisionField ?? "lastPullRev";
22695
23405
  const targetsField = revisionField === "lastPullRev" ? "lastPullTargets" : "lastInheritedPullTargets";
22696
- const teamConfig = await loadTeamConfig(localConfig.repo.localPath);
22697
- if (!teamConfig) {
22698
- log.warn(`[${scopeLabel}] Team config (teamai.yaml) not found. Skipping.`);
22699
- return;
22700
- }
22701
23406
  const pullSpin = spinner(`[${scopeLabel}] Pulling team repo...`).start();
22702
23407
  let currentRev = null;
22703
23408
  let reportingOnly = false;
@@ -22714,39 +23419,40 @@ async function pullForScope(localConfig, options, policy = {}) {
22714
23419
  pullSpin.fail(`[${scopeLabel}] Pull failed: ${e.message}`);
22715
23420
  return;
22716
23421
  }
23422
+ const freshConfig = await loadTeamConfig(localConfig.repo.localPath);
23423
+ if (!freshConfig) {
23424
+ log.warn(`[${scopeLabel}] Team config (teamai.yaml) not found. Skipping.`);
23425
+ return;
23426
+ }
22717
23427
  let currentTargets = null;
22718
23428
  if (!options.force && !options.dryRun && !submodulesChanged) {
22719
23429
  try {
22720
23430
  const state = await loadStateForScope(localConfig);
22721
23431
  if (currentRev && state[revisionField] && state[revisionField] === currentRev) {
22722
- currentTargets = await getInstalledResourceTargets(teamConfig, localConfig);
23432
+ currentTargets = await getInstalledResourceTargets(freshConfig, localConfig);
22723
23433
  const previousTargets = state[targetsField];
22724
23434
  const syncedTargets = new Set(previousTargets ?? []);
22725
23435
  const targetSetMatches = previousTargets !== void 0 && previousTargets.length === currentTargets.length && currentTargets.every((target) => syncedTargets.has(target));
22726
23436
  if (targetSetMatches) {
22727
23437
  log.success(`[${scopeLabel}] Already synced at ${currentRev}, skipping`);
22728
- if (!options.dryRun) {
22729
- const cfg = await loadTeamConfig(localConfig.repo.localPath);
22730
- if (cfg) {
22731
- const skipRecall = !isRecallEnabled(localConfig, cfg);
22732
- try {
22733
- const { deployBuiltinAgents: deployBuiltinAgents2 } = await Promise.resolve().then(() => (init_builtin_agents(), builtin_agents_exports));
22734
- await deployBuiltinAgents2(cfg, localConfig, { skipRecall });
22735
- } catch {
22736
- }
22737
- try {
22738
- const { deployBuiltinRules: deployBuiltinRules2 } = await Promise.resolve().then(() => (init_builtin_rules(), builtin_rules_exports));
22739
- await deployBuiltinRules2(cfg, localConfig, { skipRecall });
22740
- } catch {
22741
- }
22742
- try {
22743
- const { deployBuiltinSkills: deployBuiltinSkills2 } = await Promise.resolve().then(() => (init_builtin_skills(), builtin_skills_exports));
22744
- await deployBuiltinSkills2(cfg, localConfig, { reportingOnly, skipRecall });
22745
- } catch {
22746
- }
22747
- await injectRecallBlockIntoTools(cfg, localConfig, scopeLabel);
22748
- }
23438
+ const skipRecall = !isRecallEnabled(localConfig, freshConfig);
23439
+ try {
23440
+ const { deployBuiltinAgents: deployBuiltinAgents2 } = await Promise.resolve().then(() => (init_builtin_agents(), builtin_agents_exports));
23441
+ await deployBuiltinAgents2(freshConfig, localConfig, { skipRecall });
23442
+ } catch {
23443
+ }
23444
+ try {
23445
+ const { deployBuiltinRules: deployBuiltinRules2 } = await Promise.resolve().then(() => (init_builtin_rules(), builtin_rules_exports));
23446
+ await deployBuiltinRules2(freshConfig, localConfig, { skipRecall });
23447
+ } catch {
23448
+ }
23449
+ try {
23450
+ const { deployBuiltinSkills: deployBuiltinSkills2 } = await Promise.resolve().then(() => (init_builtin_skills(), builtin_skills_exports));
23451
+ await deployBuiltinSkills2(freshConfig, localConfig, { reportingOnly, skipRecall });
23452
+ } catch {
22749
23453
  }
23454
+ await injectRecallBlockIntoTools(freshConfig, localConfig, scopeLabel);
23455
+ await cleanupTombstonedResources(freshConfig, localConfig, scopeLabel);
22750
23456
  return;
22751
23457
  }
22752
23458
  log.debug(`[${scopeLabel}] Repo unchanged; resource target set changed, syncing`);
@@ -22755,11 +23461,6 @@ async function pullForScope(localConfig, options, policy = {}) {
22755
23461
  log.debug(`[${scopeLabel}] Rev check failed, proceeding with full sync`);
22756
23462
  }
22757
23463
  }
22758
- const freshConfig = await loadTeamConfig(localConfig.repo.localPath);
22759
- if (!freshConfig) {
22760
- log.warn(`[${scopeLabel}] Team config disappeared after pull. Skipping.`);
22761
- return;
22762
- }
22763
23464
  let roleContext = null;
22764
23465
  try {
22765
23466
  roleContext = await buildRolePullContext(localConfig);
@@ -22824,6 +23525,11 @@ async function pullForScope(localConfig, options, policy = {}) {
22824
23525
  desiredSkillNames = new Set(items.map((i) => i.name));
22825
23526
  knownRepoSkillNames = new Set(allTeamSkills.map((i) => i.name));
22826
23527
  knownRepoSkillSources = new Map(allTeamSkills.map((i) => [i.name, i.sourcePath]));
23528
+ } else if (type === "agents") {
23529
+ items = filterAgentsByNamespaces(
23530
+ await handler.scanTeamForPull(freshConfig, localConfig),
23531
+ roleContext ? roleContext.activeNamespaces.agents : null
23532
+ );
22827
23533
  } else {
22828
23534
  items = await handler.scanTeamForPull(freshConfig, localConfig);
22829
23535
  }
@@ -22882,37 +23588,7 @@ async function pullForScope(localConfig, options, policy = {}) {
22882
23588
  totalSynced += items.length;
22883
23589
  }
22884
23590
  if (!options.dryRun) {
22885
- const tombstoneTypes = [
22886
- { type: "rules", ext: ".md", toolPathField: "rules" },
22887
- { type: "skills", toolPathField: "skills" },
22888
- { type: "agents", ext: ".md", toolPathField: "agents" }
22889
- ];
22890
- const baseDir = resolveBaseDir(localConfig);
22891
- for (const { type, ext, toolPathField } of tombstoneTypes) {
22892
- const handler = getHandler(type);
22893
- const tombstones = await handler.readTombstones(localConfig);
22894
- if (tombstones.size === 0) continue;
22895
- for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
22896
- const dir = toolPath[toolPathField];
22897
- if (!dir) continue;
22898
- if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
22899
- if (isAgentExcluded(localConfig, tool)) continue;
22900
- const extensions = type === "rules" ? [.../* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"])] : [ext];
22901
- for (const name of tombstones) {
22902
- for (const extension of extensions) {
22903
- const localPath = path66.join(baseDir, dir, extension ? `${name}${extension}` : name);
22904
- if (await pathExists(localPath)) {
22905
- if (type === "skills" && await hasVcsMetadataRecursive(localPath)) {
22906
- 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.`);
22907
- continue;
22908
- }
22909
- await remove(localPath);
22910
- log.debug(`[${scopeLabel}] Cleaned up tombstoned ${type} ${name} from ${dir}`);
22911
- }
22912
- }
22913
- }
22914
- }
22915
- }
23591
+ await cleanupTombstonedResources(freshConfig, localConfig, scopeLabel);
22916
23592
  if (roleContext) {
22917
23593
  await cleanupInactiveNamespaceSkills(
22918
23594
  freshConfig,
@@ -22921,6 +23597,11 @@ async function pullForScope(localConfig, options, policy = {}) {
22921
23597
  roleContext.inactiveSkillNames,
22922
23598
  roleContext.inactiveSkillSources
22923
23599
  );
23600
+ await getHandler("agents").cleanupInactiveNamespaces(
23601
+ freshConfig,
23602
+ localConfig,
23603
+ roleContext.activeNamespaces.agents
23604
+ );
22924
23605
  }
22925
23606
  }
22926
23607
  if (!options.dryRun && desiredSkillNames && knownRepoSkillNames) {
@@ -22962,21 +23643,29 @@ async function pullForScope(localConfig, options, policy = {}) {
22962
23643
  if (totalSynced === 0) {
22963
23644
  log.info(`[${scopeLabel}] No resources to sync`);
22964
23645
  }
23646
+ let reportsReadRoot;
23647
+ const resolveReportsReadRoot = () => {
23648
+ reportsReadRoot ??= (async () => {
23649
+ if (!usesReportsBranch(localConfig)) return localConfig.repo.localPath;
23650
+ try {
23651
+ const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
23652
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
23653
+ return await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
23654
+ } catch (e) {
23655
+ log.debug(`reports worktree unavailable: ${e.message}`);
23656
+ return void 0;
23657
+ }
23658
+ })();
23659
+ return reportsReadRoot;
23660
+ };
22965
23661
  if (!options.dryRun) {
22966
23662
  try {
22967
23663
  const learningsRepoDir = path66.join(localConfig.repo.localPath, "learnings");
22968
23664
  const docsRepoDir = path66.join(localConfig.repo.localPath, "docs");
22969
23665
  const rulesRepoDir = path66.join(localConfig.repo.localPath, "rules");
22970
23666
  const skillsRepoDir = path66.join(localConfig.repo.localPath, "skills");
22971
- let votesDir = path66.join(localConfig.repo.localPath, "votes");
22972
- if (localConfig.repo.kind === "self") {
22973
- try {
22974
- const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22975
- votesDir = path66.join(await ensureReportsWorktree2(localConfig), "votes");
22976
- } catch (e) {
22977
- log.debug(`[self] reports worktree for votes unavailable: ${e.message}`);
22978
- }
22979
- }
23667
+ const reportsRoot = await resolveReportsReadRoot();
23668
+ const votesDir = reportsRoot ? path66.join(reportsRoot, "votes") : void 0;
22980
23669
  const activeLearningsNamespaces = roleContext?.activeNamespaces.learnings ?? [];
22981
23670
  const countLearnings = async (baseDir) => {
22982
23671
  let n = (await listFiles(baseDir)).filter((f) => f.endsWith(".md")).length;
@@ -23010,7 +23699,7 @@ async function pullForScope(localConfig, options, policy = {}) {
23010
23699
  const repoCodebaseDir = path66.join(localConfig.repo.localPath, "docs", "team-codebase");
23011
23700
  const effectiveCodebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : void 0;
23012
23701
  if (hasAnySource || effectiveCodebaseDir) {
23013
- const votesExist = await pathExists(votesDir);
23702
+ const votesExist = votesDir ? await pathExists(votesDir) : false;
23014
23703
  const teamaiHome = getDataHome(localConfig);
23015
23704
  const indexPath = path66.join(teamaiHome, "search-index.json");
23016
23705
  const { buildIndex: buildIndex2 } = await Promise.resolve().then(() => (init_search_index(), search_index_exports));
@@ -23156,16 +23845,9 @@ async function pullForScope(localConfig, options, policy = {}) {
23156
23845
  const YAML25 = (await import("yaml")).default;
23157
23846
  const { listFiles: listFiles2, readFileSafe: readFileSafe5 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
23158
23847
  const { getRecommendations: getRecommendations2, displayRecommendations: displayRecommendations2 } = await Promise.resolve().then(() => (init_skill_recommend(), skill_recommend_exports));
23159
- let statsDir = path66.join(localConfig.repo.localPath, "stats");
23160
- if (localConfig.repo.kind === "self") {
23161
- try {
23162
- const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
23163
- statsDir = path66.join(await ensureReportsWorktree2(localConfig), "stats");
23164
- } catch (e) {
23165
- log.debug(`[self] reports worktree for stats unavailable: ${e.message}`);
23166
- }
23167
- }
23168
- const files = await listFiles2(statsDir);
23848
+ const reportsRoot = await resolveReportsReadRoot();
23849
+ const statsDir = reportsRoot ? path66.join(reportsRoot, "stats") : void 0;
23850
+ const files = statsDir ? await listFiles2(statsDir) : [];
23169
23851
  const teamStats = [];
23170
23852
  for (const file of files) {
23171
23853
  if (!file.endsWith(".yaml")) continue;
@@ -23394,6 +24076,7 @@ async function pull(options) {
23394
24076
  const needsHookMigration = await legacyHooksNeedReinject().catch(() => false);
23395
24077
  const contended = /* @__PURE__ */ new Set();
23396
24078
  const heldLocks = /* @__PURE__ */ new Map();
24079
+ let usageReport;
23397
24080
  const lockScope = async (config) => {
23398
24081
  if (config.repo.kind === "http") return true;
23399
24082
  const lock = path66.join(getDataHome(config), SYNC_LOCK_FILENAME);
@@ -23469,49 +24152,62 @@ async function pull(options) {
23469
24152
  await reconcileHooksAllScopes(reconcileUser, reconcileProject, options);
23470
24153
  await reconcileMcpAllScopes(reconcileUser, reconcileProject, options);
23471
24154
  await reconcileCoAuthorAllScopes(reconcileUser, reconcileProject, options);
23472
- if (!options.dryRun) {
23473
- try {
23474
- const { reportUsageToTeam: reportUsageToTeam2 } = await Promise.resolve().then(() => (init_team_push(), team_push_exports));
23475
- const { truncateUsageAfterReport: truncateUsageAfterReport2, readUsageEvents: readUsageEvents2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
23476
- const targets = [];
23477
- if (reconcileProject && reconcileProject.repo.kind !== "http" && !await usageReportDisabled(reconcileProject.repo.localPath)) {
23478
- targets.push({
23479
- repoPath: reconcileProject.repo.localPath,
23480
- username: reconcileProject.username,
23481
- opts: {
23482
- skipTruncate: true,
23483
- projectRoot: reconcileProject.projectRoot,
23484
- // Self mode routes stats/votes to the teamai-reports orphan branch.
23485
- ...reconcileProject.repo.kind === "self" ? { selfConfig: reconcileProject } : {}
23486
- }
23487
- });
23488
- }
23489
- if (reconcileUser && reconcileUser.repo.kind !== "http" && !await usageReportDisabled(reconcileUser.repo.localPath)) {
23490
- targets.push({
23491
- repoPath: reconcileUser.repo.localPath,
23492
- username: reconcileUser.username,
23493
- opts: {
23494
- skipTruncate: true,
23495
- excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [],
23496
- // Self mode routes stats/votes to the teamai-reports orphan branch —
23497
- // never reset/pull the business repo working tree.
23498
- ...reconcileUser.repo.kind === "self" ? { selfConfig: reconcileUser } : {}
24155
+ if (!options.dryRun && !pendingUsageReport) {
24156
+ pendingUsageReport = (async () => {
24157
+ try {
24158
+ const { reportUsageToTeam: reportUsageToTeam2 } = await Promise.resolve().then(() => (init_team_push(), team_push_exports));
24159
+ const { truncateUsageAfterReport: truncateUsageAfterReport2, readUsageEvents: readUsageEvents2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
24160
+ const targets = [];
24161
+ if (reconcileProject && reconcileProject.repo.kind !== "http" && !await usageReportDisabled(reconcileProject.repo.localPath)) {
24162
+ targets.push({
24163
+ repoPath: reconcileProject.repo.localPath,
24164
+ username: reconcileProject.username,
24165
+ opts: {
24166
+ skipTruncate: true,
24167
+ projectRoot: reconcileProject.projectRoot,
24168
+ // Non-HTTP repos route stats/votes to the teamai-reports orphan branch.
24169
+ selfConfig: reconcileProject
24170
+ }
24171
+ });
24172
+ }
24173
+ if (reconcileUser && reconcileUser.repo.kind !== "http" && !await usageReportDisabled(reconcileUser.repo.localPath)) {
24174
+ targets.push({
24175
+ repoPath: reconcileUser.repo.localPath,
24176
+ username: reconcileUser.username,
24177
+ opts: {
24178
+ skipTruncate: true,
24179
+ excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [],
24180
+ // Non-HTTP repos route stats/votes to the teamai-reports orphan branch
24181
+ // never reset/pull the default branch (or, in self mode, the business tree).
24182
+ selfConfig: reconcileUser
24183
+ }
24184
+ });
24185
+ }
24186
+ const eventCount = (await readUsageEvents2()).length;
24187
+ let allReported = true;
24188
+ for (const t of targets) {
24189
+ try {
24190
+ const reported = await reportUsageToTeam2(t.repoPath, t.username, t.opts);
24191
+ if (!reported) allReported = false;
24192
+ } catch (e) {
24193
+ allReported = false;
24194
+ log.error(`Auto-report to ${t.repoPath} skipped: ${e.message}`);
23499
24195
  }
23500
- });
23501
- }
23502
- const eventCount = (await readUsageEvents2()).length;
23503
- for (const t of targets) {
23504
- try {
23505
- await reportUsageToTeam2(t.repoPath, t.username, t.opts);
23506
- } catch (e) {
23507
- log.error(`Auto-report to ${t.repoPath} skipped: ${e.message}`);
23508
24196
  }
24197
+ if (allReported && eventCount > 0 && targets.length > 0) {
24198
+ await truncateUsageAfterReport2(eventCount);
24199
+ }
24200
+ } catch (e) {
24201
+ log.debug(`Auto-report skipped: ${e.message}`);
23509
24202
  }
23510
- if (eventCount > 0 && targets.length > 0) {
23511
- await truncateUsageAfterReport2(eventCount);
23512
- }
24203
+ })().finally(() => {
24204
+ pendingUsageReport = void 0;
24205
+ });
24206
+ usageReport = pendingUsageReport;
24207
+ try {
24208
+ await withTimeout(pendingUsageReport, 5e3, "Auto-report is still running after 5s");
23513
24209
  } catch (e) {
23514
- log.debug(`Auto-report skipped: ${e.message}`);
24210
+ log.debug(e.message);
23515
24211
  }
23516
24212
  }
23517
24213
  const sourceConfig = reconcileProject ?? reconcileUser;
@@ -23524,8 +24220,15 @@ async function pull(options) {
23524
24220
  }
23525
24221
  }
23526
24222
  } finally {
23527
- for (const lock of heldLocks.values()) {
23528
- await releaseLock(lock);
24223
+ const releaseSyncLocks = async () => {
24224
+ for (const lock of heldLocks.values()) await releaseLock(lock);
24225
+ };
24226
+ if (usageReport && usageReport === pendingUsageReport) {
24227
+ void usageReport.then(releaseSyncLocks, releaseSyncLocks).catch((e) => {
24228
+ log.error(`Could not release report sync locks: ${e.message}`);
24229
+ });
24230
+ } else {
24231
+ await releaseSyncLocks();
23529
24232
  }
23530
24233
  }
23531
24234
  }
@@ -23600,7 +24303,7 @@ async function reconcileCoAuthorAllScopes(userConfig, projectConfig, options) {
23600
24303
  }
23601
24304
  }
23602
24305
  }
23603
- var CONTRIBUTORS_FILE2;
24306
+ var pendingUsageReport, CONTRIBUTORS_FILE2;
23604
24307
  var init_pull = __esm({
23605
24308
  "src/pull.ts"() {
23606
24309
  "use strict";
@@ -23613,14 +24316,15 @@ var init_pull = __esm({
23613
24316
  init_resources();
23614
24317
  init_base();
23615
24318
  init_rule_format();
24319
+ init_agent_format();
23616
24320
  init_tags();
23617
24321
  init_builtin_skills();
23618
24322
  init_types();
23619
- init_roles();
23620
- init_projects();
24323
+ init_resource_namespaces();
23621
24324
  init_home();
23622
24325
  init_update();
23623
24326
  init_learnings_mirror();
24327
+ init_async();
23624
24328
  CONTRIBUTORS_FILE2 = "CONTRIBUTORS";
23625
24329
  }
23626
24330
  });
@@ -24476,17 +25180,24 @@ async function doctor(options) {
24476
25180
  log.info("Running diagnostics...\n");
24477
25181
  const projectConfig = await detectProjectConfig();
24478
25182
  const localConfig = projectConfig ?? await loadLocalConfig();
24479
- const scope = localConfig?.scope ?? "user";
24480
- const configPathLabel = projectConfig ? `${projectConfig.projectRoot}/.teamai/config.yaml` : "~/.teamai/config.yaml";
24481
- console.log(` Scope: ${scope}${scope === "project" && localConfig?.projectRoot ? ` (${localConfig.projectRoot})` : ""}
24482
- `);
24483
- let teamConfig = null;
24484
- if (localConfig) {
24485
- teamConfig = await loadTeamConfig(localConfig.repo.localPath);
25183
+ if (!localConfig) {
25184
+ console.log(" Scope: not initialized\n");
25185
+ console.log(" \u2716 TeamAI is not initialized");
25186
+ console.log(" \u2192 Run `teamai init <repo-url>` in a project, or add `--scope user` for all projects");
25187
+ console.log("");
25188
+ log.warn("Initialization is required before diagnostics can run.");
25189
+ return false;
24486
25190
  }
24487
- const toolPaths = teamConfig?.toolPaths ?? TeamaiConfigSchema.shape.toolPaths.parse(void 0);
24488
- const providerName = teamConfig?.provider ?? "tgit";
24489
- const baseDir = localConfig ? resolveHookScope(localConfig).baseDir : getUserHome();
25191
+ const scope = localConfig.scope ?? "user";
25192
+ const scopeLabel = `${scope}${scope === "project" && localConfig.projectRoot ? ` (${localConfig.projectRoot})` : ""}`;
25193
+ console.log(` Scope: ${scopeLabel}
25194
+ `);
25195
+ const teamConfig = await loadTeamConfig(localConfig.repo.localPath);
25196
+ const toolPaths = teamConfig ? Object.fromEntries(
25197
+ Object.entries(scopedToolPaths(teamConfig, localConfig)).filter(([tool]) => !isAgentExcluded(localConfig, tool))
25198
+ ) : {};
25199
+ const providerName = teamConfig?.provider;
25200
+ const baseDir = resolveHookScope(localConfig).baseDir;
24490
25201
  const checks = [];
24491
25202
  if (providerName === "tgit") {
24492
25203
  const { isGfInstalled: isGfInstalled2, gfIsAuthenticated: gfIsAuthenticated2 } = await Promise.resolve().then(() => (init_tgit(), tgit_exports));
@@ -24532,23 +25243,14 @@ async function doctor(options) {
24532
25243
  });
24533
25244
  }
24534
25245
  checks.push(
24535
- {
24536
- name: `Local config exists (${configPathLabel})`,
24537
- check: async () => localConfig !== null,
24538
- fix: "Run `teamai init` to initialize"
24539
- },
24540
25246
  {
24541
25247
  name: "Team repo exists locally",
24542
- check: async () => {
24543
- if (!localConfig) return false;
24544
- return pathExists(localConfig.repo.localPath);
24545
- },
25248
+ check: async () => pathExists(localConfig.repo.localPath),
24546
25249
  fix: "Run `teamai init` to clone the team repo"
24547
25250
  },
24548
25251
  {
24549
25252
  name: "Team config (teamai.yaml) is valid",
24550
25253
  check: async () => {
24551
- if (!localConfig) return false;
24552
25254
  const config = await loadTeamConfig(localConfig.repo.localPath);
24553
25255
  return config !== null;
24554
25256
  },
@@ -24559,7 +25261,6 @@ async function doctor(options) {
24559
25261
  name: "Env variables injected in shell profile",
24560
25262
  check: async () => {
24561
25263
  if (teamConfig?.sharing?.env?.injectShellProfile === false) return true;
24562
- if (!localConfig) return true;
24563
25264
  const envYamlPath = path70.join(localConfig.repo.localPath, "env", "env.yaml");
24564
25265
  if (!await pathExists(envYamlPath)) return true;
24565
25266
  const home = getUserHome();
@@ -24588,13 +25289,11 @@ async function doctor(options) {
24588
25289
  allPassed = false;
24589
25290
  }
24590
25291
  }
24591
- if (localConfig) {
24592
- const { pkgDoctorReport: pkgDoctorReport2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
24593
- const packageReport = await pkgDoctorReport2(localConfig, process.cwd());
24594
- if (packageReport) {
24595
- for (const line of packageReport.lines) console.log(line);
24596
- if (!packageReport.allPassed) allPassed = false;
24597
- }
25292
+ const { pkgDoctorReport: pkgDoctorReport2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
25293
+ const packageReport = await pkgDoctorReport2(localConfig, process.cwd());
25294
+ if (packageReport) {
25295
+ for (const line of packageReport.lines) console.log(line);
25296
+ if (!packageReport.allPassed) allPassed = false;
24598
25297
  }
24599
25298
  if (await hasInstalledCodexHooks(toolPaths, baseDir)) {
24600
25299
  console.log("");
@@ -24606,6 +25305,7 @@ async function doctor(options) {
24606
25305
  } else {
24607
25306
  log.warn("Some checks failed. See suggestions above.");
24608
25307
  }
25308
+ return allPassed;
24609
25309
  }
24610
25310
  var init_doctor = __esm({
24611
25311
  "src/doctor.ts"() {
@@ -24730,7 +25430,8 @@ async function rolesInit(options) {
24730
25430
  description,
24731
25431
  resources: {
24732
25432
  knowledge: namespaces,
24733
- skills: namespaces
25433
+ skills: namespaces,
25434
+ agents: namespaces
24734
25435
  }
24735
25436
  });
24736
25437
  log.success(`Added role: ${id} (namespaces: ${namespaces.join(", ")})`);
@@ -24874,7 +25575,8 @@ async function rolesAdd(roleId, options) {
24874
25575
  description: options.description ?? "",
24875
25576
  resources: {
24876
25577
  knowledge: namespaces,
24877
- skills: namespaces
25578
+ skills: namespaces,
25579
+ agents: namespaces
24878
25580
  }
24879
25581
  };
24880
25582
  const updatedManifest = {
@@ -24988,7 +25690,8 @@ async function rolesUpdate(roleId, options) {
24988
25690
  description: hasDesc ? options.description : existingRole.description,
24989
25691
  resources: {
24990
25692
  knowledge: updatedNamespaces,
24991
- skills: updatedNamespaces
25693
+ skills: updatedNamespaces,
25694
+ agents: updatedNamespaces
24992
25695
  }
24993
25696
  };
24994
25697
  const updatedManifest = {
@@ -25120,21 +25823,22 @@ async function projectsSet(ids, _options) {
25120
25823
  }
25121
25824
  async function projectsMembers(projectId, _options) {
25122
25825
  const { localConfig } = await autoDetectInit();
25123
- let repoPath;
25124
- if (localConfig.repo.kind === "self") {
25826
+ const knowledgePath = localConfig.repo.localPath;
25827
+ let membersRoot = knowledgePath;
25828
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
25829
+ if (usesReportsBranch2(localConfig)) {
25125
25830
  const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
25126
- await refreshReportsWorktree2(localConfig);
25127
- repoPath = await ensureReportsWorktree2(localConfig);
25831
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
25832
+ membersRoot = await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
25128
25833
  } else {
25129
- repoPath = localConfig.repo.localPath;
25130
- await pullRepo(repoPath).catch(() => {
25834
+ await pullRepo(knowledgePath).catch(() => {
25131
25835
  });
25132
25836
  }
25133
- const manifest = await loadProjectsManifest(repoPath);
25837
+ const manifest = await loadProjectsManifest(knowledgePath);
25134
25838
  if (manifest && !listProjectIds(manifest).includes(projectId)) {
25135
25839
  log.warn(`Project "${projectId}" is not defined in manifest/projects.yaml.`);
25136
25840
  }
25137
- const membersDir = path72.join(repoPath, "members");
25841
+ const membersDir = path72.join(membersRoot, "members");
25138
25842
  const files = (await listFiles(membersDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
25139
25843
  const members = [];
25140
25844
  for (const file of files) {
@@ -25410,10 +26114,13 @@ async function collectTeamRuleNames(repoPath) {
25410
26114
  async function collectTeamAgentNames(repoPath) {
25411
26115
  const teamAgentsDir = path74.join(repoPath, "agents");
25412
26116
  if (!await pathExists(teamAgentsDir)) return /* @__PURE__ */ new Set();
25413
- const files = await listFiles(teamAgentsDir);
25414
- return new Set(
25415
- files.filter((file) => file.endsWith(".yaml") || file.endsWith(".md")).map((file) => path74.basename(file).replace(/\.(yaml|md)$/, ""))
25416
- );
26117
+ const names = /* @__PURE__ */ new Set();
26118
+ for (const { dir } of await listTeamAgentDirs(teamAgentsDir)) {
26119
+ for (const file of await listFiles(dir)) {
26120
+ if (file.endsWith(".yaml") || file.endsWith(".md")) names.add(file.replace(/\.(yaml|md)$/, ""));
26121
+ }
26122
+ }
26123
+ return names;
25417
26124
  }
25418
26125
  function isEmptyHooksResidue(parsed) {
25419
26126
  if (parsed == null || !("hooks" in parsed) || typeof parsed.hooks !== "object" || parsed.hooks == null) return false;
@@ -25513,8 +26220,8 @@ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, te
25513
26220
  const agentsDir = path74.join(baseDir, toolPath.agents);
25514
26221
  if (await pathExists(agentsDir)) {
25515
26222
  for (const file of await listFiles(agentsDir)) {
25516
- if (!file.endsWith(".md") && !file.endsWith(".toml")) continue;
25517
- const name = path74.basename(file).replace(/\.(md|toml)$/, "");
26223
+ const name = agentStemFromFilename(path74.basename(file));
26224
+ if (name === null) continue;
25518
26225
  if (!teamAgentNames.has(name) && !BUILTIN_AGENT_NAMES.has(name)) continue;
25519
26226
  res.agentFiles.push(path74.join(agentsDir, file));
25520
26227
  }
@@ -25992,6 +26699,8 @@ var init_uninstall = __esm({
25992
26699
  init_types();
25993
26700
  init_builtin_rules();
25994
26701
  init_rule_format();
26702
+ init_agent_format();
26703
+ init_agents();
25995
26704
  init_builtin_agents();
25996
26705
  init_builtin_skills();
25997
26706
  init_fs();
@@ -26209,7 +26918,8 @@ async function hooksList(_options) {
26209
26918
  for (const d of teamDefs) {
26210
26919
  const matcher = d.matcher ? ` [${d.matcher}]` : "";
26211
26920
  const tools = d.tools && d.tools.length > 0 ? d.tools.join(",") : "all";
26212
- console.log(` [${d.key}] ${d.event}${matcher} \u2192 ${d.command} (tools: ${tools})`);
26921
+ const roles = d.roles ? `, roles: ${d.roles.length > 0 ? d.roles.join(",") : "nobody"}` : "";
26922
+ console.log(` [${d.key}] ${d.event}${matcher} \u2192 ${d.command} (tools: ${tools}${roles})`);
26213
26923
  }
26214
26924
  }
26215
26925
  console.log("");
@@ -26269,6 +26979,7 @@ async function mcpList(_options) {
26269
26979
  console.log(` ${s.name} [${s.transport}]`);
26270
26980
  if (s.description) console.log(` ${s.description}`);
26271
26981
  console.log(` endpoint: ${endpoint}`);
26982
+ if (s.roles) console.log(` roles: ${s.roles.length > 0 ? s.roles.join(", ") : "nobody"}`);
26272
26983
  const needed = referencedVars(s);
26273
26984
  if (needed.length > 0) {
26274
26985
  const missing = needed.filter((v) => !vars[v]);
@@ -26539,23 +27250,28 @@ async function saveSession(options) {
26539
27250
  log.info(`[dry-run] Would push session summary to sessions/${username}/${monthKey(summary)}.md`);
26540
27251
  return;
26541
27252
  }
26542
- if (localConfig.repo.kind === "self") {
27253
+ if (usesReportsBranch(localConfig)) {
26543
27254
  const spin2 = spinner("Pushing session summary to team...").start();
26544
27255
  try {
26545
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
26546
- const wt = await ensureReportsWorktree2(localConfig);
26547
- const teamDir2 = path79.join(wt, "sessions", username);
26548
- const written = await appendMonthlyLog(teamDir2, summary, { includePrompt: options.includePrompt });
26549
- if (!written) {
26550
- spin2.info("Session already present in the team log \u2014 nothing to push.");
26551
- return;
26552
- }
26553
- const rel = path79.relative(wt, written);
27256
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
27257
+ let rel;
27258
+ let ran = false;
26554
27259
  const pushed = await withTimeout(
26555
- commitAndPushReports2(localConfig, commitMsg, [rel]),
27260
+ updateReports2(localConfig, async (wt) => {
27261
+ ran = true;
27262
+ const teamDir2 = path79.join(wt, "sessions", username);
27263
+ const written = await appendMonthlyLog(teamDir2, summary, { includePrompt: options.includePrompt });
27264
+ if (!written) return null;
27265
+ rel = path79.relative(wt, written);
27266
+ return { files: [rel], message: commitMsg };
27267
+ }),
26556
27268
  1e4,
26557
27269
  "Push timeout (10s)"
26558
27270
  );
27271
+ if (ran && !rel) {
27272
+ spin2.info("Session already present in the team log \u2014 nothing to push.");
27273
+ return;
27274
+ }
26559
27275
  if (pushed) spin2.succeed(`Pushed: ${rel}`);
26560
27276
  else spin2.info("Nothing new to push.");
26561
27277
  } catch (e) {
@@ -28670,9 +29386,10 @@ async function resolveVizRoot(opts) {
28670
29386
  }
28671
29387
  const config = await detectProjectConfig() ?? await loadLocalConfig();
28672
29388
  if (config?.repo?.localPath) {
28673
- if (config.repo.kind === "self") {
29389
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
29390
+ if (usesReportsBranch2(config)) {
28674
29391
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
28675
- await ensureReportsWorktree2(config);
29392
+ await ensureReportsWorktree2(config, { pushIfCreated: false });
28676
29393
  }
28677
29394
  const knowledgeRoot = getKnowledgeDir(config);
28678
29395
  const reportsRoot = getReportsDir(config);
@@ -30439,6 +31156,17 @@ var init_mr_hint = __esm({
30439
31156
 
30440
31157
  // src/hook-handlers.ts
30441
31158
  import path92 from "path";
31159
+ async function teamCorrectionKeywords(stdin) {
31160
+ if (typeof stdin.prompt !== "string") return [];
31161
+ try {
31162
+ const { autoDetectInit: autoDetectInit2 } = await Promise.resolve().then(() => (init_config(), config_exports));
31163
+ const { getInterventionSharing: getInterventionSharing2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31164
+ const { teamConfig } = await autoDetectInit2();
31165
+ return getInterventionSharing2(teamConfig).correctionKeywords;
31166
+ } catch {
31167
+ return [];
31168
+ }
31169
+ }
30442
31170
  async function contributeHintAllowed() {
30443
31171
  const { isContributeHintEnabled: isContributeHintEnabled2 } = await Promise.resolve().then(() => (init_types(), types_exports));
30444
31172
  try {
@@ -30539,7 +31267,7 @@ var init_hook_handlers = __esm({
30539
31267
  async execute(stdin, tool) {
30540
31268
  const { parseHookEvent: parseHookEvent2, appendEvent: appendEvent2, compactEvents: compactEvents2 } = await Promise.resolve().then(() => (init_dashboard_collector(), dashboard_collector_exports));
30541
31269
  const raw = JSON.stringify(stdin);
30542
- const event = await parseHookEvent2(raw, tool);
31270
+ const event = await parseHookEvent2(raw, tool, { correctionKeywords: await teamCorrectionKeywords(stdin) });
30543
31271
  if (event) {
30544
31272
  await appendEvent2(event);
30545
31273
  compactEvents2().catch(() => {
@@ -30664,13 +31392,16 @@ var init_hook_handlers = __esm({
30664
31392
  if (verifiedDocIds.length > 0) {
30665
31393
  await incrementUpvoted2(votePath, verifiedDocIds);
30666
31394
  }
30667
- if (localConfig.repo.kind === "self") {
31395
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31396
+ if (usesReportsBranch2(localConfig)) {
30668
31397
  try {
30669
- const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
30670
- const wt = await ensureReportsWorktree2(localConfig);
30671
- const synced = await syncVotesToTeam2(wt, localConfig.username, votesDir);
30672
- if (synced) {
30673
- await commitAndPushReports2(localConfig, `[teamai] Update votes for ${localConfig.username}`, [`votes/${localConfig.username}.yaml`]);
31398
+ const { hasPendingVoteDeltas: hasPendingVoteDeltas2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
31399
+ if (await hasPendingVoteDeltas2(votesDir, localConfig.username)) {
31400
+ const { updateReports: updateReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
31401
+ await updateReports2(localConfig, async (wt) => await syncVotesToTeam2(wt, localConfig.username, votesDir) ? {
31402
+ files: [`votes/${localConfig.username}.yaml`],
31403
+ message: `[teamai] Update votes for ${localConfig.username}`
31404
+ } : null);
30674
31405
  }
30675
31406
  } catch {
30676
31407
  }
@@ -30767,7 +31498,8 @@ var init_hook_handlers = __esm({
30767
31498
  // src/hook-dispatch-cli.ts
30768
31499
  var hook_dispatch_cli_exports = {};
30769
31500
  __export(hook_dispatch_cli_exports, {
30770
- hookDispatchCli: () => hookDispatchCli
31501
+ hookDispatchCli: () => hookDispatchCli,
31502
+ parseStdin: () => parseStdin
30771
31503
  });
30772
31504
  import { spawn as spawn3 } from "child_process";
30773
31505
  async function readStdin4() {
@@ -30828,10 +31560,15 @@ function parseStdin(raw, event) {
30828
31560
  try {
30829
31561
  stdin = JSON.parse(raw);
30830
31562
  } catch {
30831
- log.debug(`hook-dispatch: failed to parse STDIN JSON for event=${event}`);
30832
- return null;
31563
+ const preview = raw.length > 160 ? `${raw.slice(0, 80)}...${raw.slice(-80)}` : raw;
31564
+ log.debug(
31565
+ `hook-dispatch: failed to parse STDIN JSON for event=${event} (len=${raw.length}, body=${JSON.stringify(preview)})`
31566
+ );
30833
31567
  }
30834
31568
  }
31569
+ if (!stdin || typeof stdin !== "object" || Array.isArray(stdin)) {
31570
+ stdin = {};
31571
+ }
30835
31572
  if (!stdin.hook_event_name) {
30836
31573
  const EVENT_MAP2 = {
30837
31574
  "session-start": "SessionStart",
@@ -30857,7 +31594,6 @@ async function hookDispatchCli(event, tool, matcher, bgOnly = false) {
30857
31594
  try {
30858
31595
  const raw = await readStdin4();
30859
31596
  const stdin = parseStdin(raw, event);
30860
- if (stdin === null) return;
30861
31597
  const { loadLocalConfig: loadLocalConfig4, detectProjectConfig: detectProjectConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
30862
31598
  const cwd = resolveHookCwd(stdin);
30863
31599
  if (cwd) {
@@ -30925,7 +31661,8 @@ async function rebuildIndexAfterContribute(localConfig) {
30925
31661
  const docsRepoDir = path93.join(repoPath, "docs");
30926
31662
  const rulesRepoDir = path93.join(repoPath, "rules");
30927
31663
  const skillsRepoDir = path93.join(repoPath, "skills");
30928
- const votesDir = path93.join(repoPath, "votes");
31664
+ const { getReportsDir: getReportsDir2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31665
+ const votesDir = path93.join(getReportsDir2(localConfig), "votes");
30929
31666
  let effectiveLearningsDir;
30930
31667
  const activeLearningsNamespaces = await resolveActiveLearningsNamespaces(
30931
31668
  repoPath,
@@ -31075,7 +31812,7 @@ async function contributeSelf(localConfig, content, options) {
31075
31812
  let votesDir;
31076
31813
  try {
31077
31814
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
31078
- const candidate = path93.join(await ensureReportsWorktree2(localConfig), "votes");
31815
+ const candidate = path93.join(await ensureReportsWorktree2(localConfig, { pushIfCreated: false }), "votes");
31079
31816
  if (await pathExists3(candidate)) votesDir = candidate;
31080
31817
  } catch {
31081
31818
  }
@@ -34384,7 +35121,7 @@ var init_codebase = __esm({
34384
35121
 
34385
35122
  // src/wiki-engine/code-knowledge/code-collector.ts
34386
35123
  import { createHash as createHash5 } from "crypto";
34387
- import { execFile as execFile4 } from "child_process";
35124
+ import { execFile as execFile3 } from "child_process";
34388
35125
  import { readFile as readFile6, readdir as readdir2, stat as stat2 } from "fs/promises";
34389
35126
  import path105 from "path";
34390
35127
  import { promisify as promisify3 } from "util";
@@ -34540,7 +35277,7 @@ var init_code_collector = __esm({
34540
35277
  "src/wiki-engine/code-knowledge/code-collector.ts"() {
34541
35278
  "use strict";
34542
35279
  init_wiki_protocol();
34543
- execFileAsync3 = promisify3(execFile4);
35280
+ execFileAsync3 = promisify3(execFile3);
34544
35281
  KEY_FILE_PATTERNS = {
34545
35282
  go: [/main\.go$/, /cmd\/.*\.go$/, /handler.*\.go$/, /server\.go$/, /router\.go$/],
34546
35283
  python: [/main\.py$/, /app\.py$/, /server\.py$/, /routes?\.py$/, /models?\.py$/],
@@ -42412,7 +43149,7 @@ var init_extract_mr = __esm({
42412
43149
  // src/maintenance/paths.ts
42413
43150
  import path130 from "path";
42414
43151
  async function resolveMaintenancePaths(localConfig) {
42415
- if (isSelfMode(localConfig)) {
43152
+ if (usesReportsBranch(localConfig)) {
42416
43153
  const { refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
42417
43154
  await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
42418
43155
  }
@@ -42509,7 +43246,7 @@ program.name("teamai").description("TeamAI \u2014 Make Every Team AI Native").ve
42509
43246
  process.exit(1);
42510
43247
  }
42511
43248
  });
42512
- program.command("init").description("Initialize teamai (configure Git provider, clone repo, register member)").argument("[repo]", 'Team repo (owner/repo or full URL). Pass "." for single-repo mode (the current git repo is the team repo).').option("--repo <repo>", "Team repo (alias of the positional argument)").option("--http <url>", "Git-free HTTP team repo (read-only consumer; only needs an API key)").option("--self", "Single-repo mode: the current git repo is the team repo (equivalent to `teamai init .`). Knowledge lives on main under .teamai/; reports go to the teamai-reports orphan branch.").option("--token <key>", "API key for HTTP team repo / status reporting (stored 0600, never committed). Also reads TEAMAI_API_TOKEN.").option("--scope <scope>", "Install scope: project (default, <cwd>/.teamai + <cwd>/.claude) or user (~/.teamai + ~/.claude)").option("--inherit-user-scope", "In project scope, also sync safe user-scope resources and search its knowledge").option("--no-inherit-user-scope", "Disable user-scope inheritance for this project").option("--role <id>", "Primary role ID (e.g. hai_dev) for non-interactive setup").option("--project <ids>", "Active logical project(s) from manifest/projects.yaml (comma-separated); scopes which project resources and learnings this directory syncs").option("--agent <name>", "AI tools to set up (e.g. claude, codex, cursor, codebuddy, workbuddy, dsh). Repeatable or comma-separated. In single-repo mode, selects which tool dirs to create; omit for an interactive picker. Additive on repeated runs.", (val, acc) => acc.concat(val), []).option("--force", "Overwrite existing config without confirmation").action(async (repoArg, cmdOpts) => {
43249
+ program.command("init").description("Initialize teamai (configure Git provider, clone repo, register member)").argument("[repo]", 'Team repo (owner/repo or full URL). Pass "." for single-repo mode (the current git repo is the team repo).').option("--repo <repo>", "Team repo (alias of the positional argument)").option("--http <url>", "Git-free HTTP team repo (read-only consumer; only needs an API key)").option("--self", "Single-repo mode: the current git repo is the team repo (equivalent to `teamai init .`). Knowledge lives on main under .teamai/; reports go to the teamai-reports orphan branch.").option("--token <key>", "API key for HTTP team repo / status reporting (stored 0600, never committed). Also reads TEAMAI_API_TOKEN.").option("--scope <scope>", "Install scope: project (default, <cwd>/.teamai + <cwd>/.claude) or user (~/.teamai + ~/.claude)").option("--inherit-user-scope", "In project scope, also sync safe user-scope resources and search its knowledge").option("--no-inherit-user-scope", "Disable user-scope inheritance for this project").option("--role <id>", "Primary role ID (e.g. hai_dev) for non-interactive setup").option("--project <ids>", 'Active logical project(s) from manifest/projects.yaml (comma-separated); scopes which project resources and learnings this directory syncs. Pass "all" to activate every project the manifest declares (a snapshot taken now)').option("--agent <name>", "AI tools to set up (e.g. claude, codex, cursor, codebuddy, workbuddy, dsh). Repeatable or comma-separated. In single-repo mode, selects which tool dirs to create; omit for an interactive picker. Additive on repeated runs.", (val, acc) => acc.concat(val), []).option("--force", "Overwrite existing config without confirmation").action(async (repoArg, cmdOpts) => {
42513
43250
  const globalOpts = program.opts();
42514
43251
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
42515
43252
  await init2({ ...globalOpts, ...cmdOpts, repoPositional: repoArg });
@@ -42589,7 +43326,8 @@ registerPackagesCommand(program);
42589
43326
  program.command("doctor").description("Diagnose configuration issues").action(async () => {
42590
43327
  const globalOpts = program.opts();
42591
43328
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
42592
- await doctor2(globalOpts);
43329
+ const allPassed = await doctor2(globalOpts);
43330
+ if (!allPassed) process.exitCode = 1;
42593
43331
  });
42594
43332
  var rolesCmd = program.command("roles").description("Manage team roles and resource namespaces").action(async () => {
42595
43333
  const globalOpts = program.opts();
@@ -42890,7 +43628,7 @@ program.command("todowrite-hint", { hidden: true }).description("Remind the agen
42890
43628
  await todoWriteHint2();
42891
43629
  }
42892
43630
  });
42893
- program.command("import").description("Import knowledge from local directories, remote repos, organizations, MRs, or iWiki").option("--dir <path>", "Extract code knowledge from a local directory (same as --from-repo but no clone)").addOption(new Option("--from-claude", "Scan Claude/Cursor rule directories (~/.claude/rules, ~/.cursor/rules)").hideHelp()).option("--from-mr <url>", "Extract learning from merged MR/PR and trigger incremental teamwiki update").option("--from-iwiki <space-id-or-url>", "Import documents from iWiki Space ID or page URL (requires TAI_PAT_TOKEN)").addOption(new Option("--resume", "Resume an interrupted import session").hideHelp()).option("--all", "Accept all suggestions without interactive confirmation").addOption(new Option("--output <path>", "Write drafts to this directory instead of pushing to team repo").hideHelp()).option("--from-repo <url>", "Clone a remote repo and generate per-repo codebase summary").addOption(new Option("--ssh", "Force SSH clone even if HTTPS token is available").hideHelp()).addOption(new Option("--domain <name>", "Skip AI recommendation and assign repo to this domain explicitly").hideHelp()).option("--from-repo-list <path>", "Batch import repos from a YAML whitelist").addOption(new Option("--concurrency <n>", "Concurrent repos for --from-repo-list (default 3)").default("3").hideHelp()).option("--incremental", "Use cached clone with fetch+reset (with --from-repo or --from-repo-list)").option("--skip-enrich", "Skip AI enrichment (only clone + extract + graph, no LLM calls)").option("--from-org <org>", "List repos under an org and generate a repo whitelist").addOption(new Option("--max-repos <n>", "Cap on repos pulled from --from-org (default 200)").default("200").hideHelp()).addOption(new Option("--exclude-archived", "Exclude archived repos from --from-org (default true)").hideHelp()).addOption(new Option("--include-pattern <re>", "Regex to include repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--exclude-pattern <re>", "Regex to exclude repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--skip-import", "Only write drafts; skip the actual --from-repo-list run").hideHelp()).addOption(new Option("--iwiki-dual", "Enable dual-output mode for --from-iwiki (write codebase sections in addition to learning)").hideHelp()).addOption(new Option("--require-review", "Defer codebase section writes to .teamai/pending-review.jsonl for human review").hideHelp()).option("--cache-status", "Show import cache status (repos cached, disk usage)").option("--cache-gc", "Garbage-collect stale import cache entries").addOption(new Option("--max-bytes <n>", "Override capacity cap for --cache-gc").hideHelp()).addOption(new Option("--stale-days <n>", "Threshold for stale-eviction in days (default 30)").default("30").hideHelp()).action(async (cmdOpts) => {
43631
+ program.command("import").description("Import knowledge from local directories, remote repos, organizations, MRs, or iWiki").option("--dir <path>", "Extract code knowledge from a local directory (same as --from-repo but no clone)").addOption(new Option("--from-claude", "Scan Claude/Cursor rule directories (~/.claude/rules, ~/.cursor/rules)").hideHelp()).option("--from-mr <url>", "Extract learning from merged MR/PR and trigger incremental teamwiki update").option("--from-iwiki <space-id-or-url>", "Import documents from iWiki Space ID or page URL (requires TAI_PAT_TOKEN)").addOption(new Option("--resume", "Resume an interrupted import session").hideHelp()).option("--all", "Accept all suggestions without interactive confirmation").addOption(new Option("--output <path>", "Write drafts to this directory instead of pushing to team repo").hideHelp()).option("--from-repo <url>", "Clone a remote repo and generate per-repo codebase summary").addOption(new Option("--ssh", "Force SSH clone even if HTTPS token is available").hideHelp()).addOption(new Option("--domain <name>", "Skip AI recommendation and assign repo to this domain explicitly").hideHelp()).option("--from-repo-list <path>", "Batch import repos from a YAML whitelist").addOption(new Option("--concurrency <n>", "Concurrent repos for --from-repo-list (default 3)").default("3").hideHelp()).option("--incremental", "Use cached clone with fetch+reset (with --from-repo or --from-repo-list)").option("--skip-enrich", "Skip AI enrichment (only clone + extract + graph, no LLM calls)").option("--from-org <org>", "List repos under an org and generate a repo whitelist").addOption(new Option("--max-repos <n>", "Cap on repos pulled from --from-org (default 200)").default("200").hideHelp()).addOption(new Option("--exclude-archived", "Exclude archived repos from --from-org (default true)").hideHelp()).addOption(new Option("--include-pattern <re>", "Regex to include repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--exclude-pattern <re>", "Regex to exclude repos by full name (used with --from-org)").hideHelp()).addOption(new Option("--skip-import", "Only write drafts; skip the actual --from-repo-list run").hideHelp()).addOption(new Option("--iwiki-dual", "Enable dual-output mode for --from-iwiki (write codebase sections in addition to learning)").hideHelp()).addOption(new Option("--require-review", "Defer codebase section writes to .teamai/pending-review.jsonl for human review").hideHelp()).option("--cache-status", "Show import cache status (repos cached, disk usage)").option("--cache-gc", "Garbage-collect stale import cache entries").option("--json", "Output cache status or GC result as JSON").addOption(new Option("--max-bytes <n>", "Override capacity cap for --cache-gc").hideHelp()).addOption(new Option("--stale-days <n>", "Threshold for stale-eviction in days (default 30)").default("30").hideHelp()).action(async (cmdOpts) => {
42894
43632
  const globalOpts = program.opts();
42895
43633
  if (cmdOpts.cacheStatus || cmdOpts.cacheGc) {
42896
43634
  const { cacheCmd: cacheCmd2 } = await Promise.resolve().then(() => (init_cache_cmd(), cache_cmd_exports));