teamai-cli 0.24.0 → 0.25.0-beta.0

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,174 @@ 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
+ };
1349
+ for (const type of ROLE_RESOURCE_TYPES) {
1350
+ const seen = /* @__PURE__ */ new Set();
1351
+ for (const role of resolvedRoles) {
1352
+ for (const namespace of role.resources[type]) {
1353
+ if (seen.has(namespace)) continue;
1354
+ seen.add(namespace);
1355
+ namespaces[type].push(namespace);
1356
+ }
1357
+ }
1358
+ }
1359
+ return namespaces;
1360
+ }
1361
+ function activeRoleIds(localConfig) {
1362
+ if (!localConfig.primaryRole) return null;
1363
+ return [.../* @__PURE__ */ new Set([localConfig.primaryRole, ...localConfig.additionalRoles ?? []])];
1364
+ }
1365
+ function matchesRoles(entryRoles, active) {
1366
+ if (!entryRoles || active == null) return true;
1367
+ return entryRoles.some((role) => active.includes(role));
1368
+ }
1369
+ async function warnUnknownRoleIds(repoPath, file, entries) {
1370
+ if (!entries.some((entry) => entry.roles && entry.roles.length > 0)) return;
1371
+ let known;
1372
+ try {
1373
+ known = new Set(listRoleIds(await loadRolesManifest(repoPath)));
1374
+ } catch {
1375
+ return;
1376
+ }
1377
+ for (const entry of entries) {
1378
+ for (const role of entry.roles ?? []) {
1379
+ if (known.has(role) || reportedUnknownRoles.has(`${file}:${role}`)) continue;
1380
+ reportedUnknownRoles.add(`${file}:${role}`);
1381
+ log.warn(`roles: unknown role id "${role}" in ${file} ${entry.kind} "${entry.name}". Valid roles: ${[...known].join(", ")}`);
1382
+ }
1383
+ }
1384
+ }
1385
+ var ROLE_RESOURCE_TYPES, RoleResourceNamespacesSchema, RoleSchema, RolesManifestSchema, reportedUnknownRoles;
1386
+ var init_roles = __esm({
1387
+ "src/roles.ts"() {
1388
+ "use strict";
1389
+ init_fs();
1390
+ init_logger();
1391
+ ROLE_RESOURCE_TYPES = ["knowledge", "skills"];
1392
+ RoleResourceNamespacesSchema = z2.object({
1393
+ knowledge: z2.array(z2.string().min(1)),
1394
+ skills: z2.array(z2.string().min(1)),
1395
+ // learnings is accepted for backward compatibility but ignored at runtime.
1396
+ // All learnings are shared flat across the entire team (no namespace isolation).
1397
+ learnings: z2.array(z2.string()).optional()
1398
+ });
1399
+ RoleSchema = z2.object({
1400
+ id: z2.string().min(1),
1401
+ description: z2.string().default(""),
1402
+ resources: RoleResourceNamespacesSchema
1403
+ });
1404
+ RolesManifestSchema = z2.object({
1405
+ version: z2.number(),
1406
+ roles: z2.array(RoleSchema).min(1),
1407
+ // defaults.shareTarget was removed: learnings are flat, no namespace routing needed.
1408
+ // Old manifests with a defaults block are still parseable (z.passthrough on object level).
1409
+ defaults: z2.object({}).passthrough().optional()
1410
+ });
1411
+ reportedUnknownRoles = /* @__PURE__ */ new Set();
1412
+ }
1413
+ });
1414
+
1215
1415
  // src/bundled-runtime.ts
1216
1416
  import fs2 from "fs";
1217
- import path4 from "path";
1417
+ import path5 from "path";
1218
1418
  function compareSemver(a, b) {
1219
1419
  const aParts = a.split(".").map((s) => parseInt(s, 10) || 0);
1220
1420
  const bParts = b.split(".").map((s) => parseInt(s, 10) || 0);
@@ -1230,18 +1430,18 @@ function pickLatestVersion(versions) {
1230
1430
  return versions.reduce((best, v) => compareSemver(v, best) > 0 ? v : best, versions[0]);
1231
1431
  }
1232
1432
  function latestVersionDir(relDir) {
1233
- const versionsDir = path4.join(getUserHome(), relDir);
1433
+ const versionsDir = path5.join(getUserHome(), relDir);
1234
1434
  try {
1235
1435
  const versions = fs2.readdirSync(versionsDir).filter((d) => !d.startsWith("."));
1236
1436
  const latest = pickLatestVersion(versions);
1237
- return latest ? path4.join(versionsDir, latest) : null;
1437
+ return latest ? path5.join(versionsDir, latest) : null;
1238
1438
  } catch {
1239
1439
  return null;
1240
1440
  }
1241
1441
  }
1242
1442
  function resolveWorkbuddyNode() {
1243
1443
  const dir = latestVersionDir(WORKBUDDY_BUNDLED_NODE_DIR);
1244
- const nodeBin = dir && path4.join(dir, "bin", "node");
1444
+ const nodeBin = dir && path5.join(dir, "bin", "node");
1245
1445
  return nodeBin && fs2.existsSync(nodeBin) ? nodeBin : null;
1246
1446
  }
1247
1447
  function resolveCodebuddyNode() {
@@ -1251,10 +1451,10 @@ function resolveCodebuddyNode() {
1251
1451
  for (const entry of entries) {
1252
1452
  if (!entry.startsWith(".codebuddy-server")) continue;
1253
1453
  try {
1254
- const binDir = path4.join(home, entry, "bin");
1454
+ const binDir = path5.join(home, entry, "bin");
1255
1455
  const stableDirs = fs2.readdirSync(binDir).filter((d) => d.startsWith("stable-"));
1256
1456
  for (const stable of stableDirs) {
1257
- const nodeBin = path4.join(binDir, stable, "node");
1457
+ const nodeBin = path5.join(binDir, stable, "node");
1258
1458
  if (fs2.existsSync(nodeBin)) return nodeBin;
1259
1459
  }
1260
1460
  } catch {
@@ -1270,8 +1470,8 @@ function resolveWorkbuddyShell() {
1270
1470
  if (process.platform === "win32") {
1271
1471
  const dir = latestVersionDir(WORKBUDDY_PORTABLE_GIT_DIR);
1272
1472
  if (dir) {
1273
- for (const rel of ["bin", path4.join("usr", "bin")]) {
1274
- const shBin = path4.join(dir, rel, "sh.exe");
1473
+ for (const rel of ["bin", path5.join("usr", "bin")]) {
1474
+ const shBin = path5.join(dir, rel, "sh.exe");
1275
1475
  if (fs2.existsSync(shBin)) {
1276
1476
  _wbShellCache = shBin;
1277
1477
  break;
@@ -1301,7 +1501,7 @@ var init_bundled_runtime = __esm({
1301
1501
 
1302
1502
  // src/builtin-hooks.ts
1303
1503
  import fs3 from "fs";
1304
- import path5 from "path";
1504
+ import path6 from "path";
1305
1505
  import { fileURLToPath } from "url";
1306
1506
  function hasShell() {
1307
1507
  if (_hasShellCache === void 0) {
@@ -1316,8 +1516,8 @@ function hasShell() {
1316
1516
  function resolveTeamaiEntryScript() {
1317
1517
  try {
1318
1518
  const thisFile = fileURLToPath(import.meta.url);
1319
- const distDir = path5.dirname(thisFile);
1320
- const candidate = path5.join(distDir, "index.js");
1519
+ const distDir = path6.dirname(thisFile);
1520
+ const candidate = path6.join(distDir, "index.js");
1321
1521
  if (fs3.existsSync(candidate)) return candidate;
1322
1522
  } catch {
1323
1523
  }
@@ -1328,8 +1528,8 @@ function ensureTeamaiWrapper() {
1328
1528
  if (!entryScript) return null;
1329
1529
  const nodeBin = resolveWorkbuddyNode() ?? resolveCodebuddyNode() ?? process.argv[0];
1330
1530
  const home = getUserHome();
1331
- const binDir = path5.join(home, TEAMAI_BIN_DIR);
1332
- const wrapperPath = path5.join(binDir, WRAPPER_NAME);
1531
+ const binDir = path6.join(home, TEAMAI_BIN_DIR);
1532
+ const wrapperPath = path6.join(binDir, WRAPPER_NAME);
1333
1533
  const script = [
1334
1534
  "#!/bin/sh",
1335
1535
  `# Auto-generated by teamai \u2014 do not edit.`,
@@ -1383,7 +1583,7 @@ function getWrapperDispatchCommand(event, tool, matcher) {
1383
1583
  return `PATH="$HOME/${TEAMAI_BIN_DIR}:$PATH" teamai hook-dispatch ${event} --tool ${tool}${matcherArg} 2>/dev/null || true`;
1384
1584
  }
1385
1585
  function builtinHookDefs(tool) {
1386
- const withTimeout3 = tool === "cursor" || tool === "workbuddy" || tool === "codebuddy" || tool === "zcode";
1586
+ const withTimeout3 = tool === "cursor" || tool === "workbuddy" || tool === "codebuddy";
1387
1587
  const buildCommand = tool === "zcode" ? getRawDispatchCommand : WRAPPER_TOOLS.has(tool) ? getWrapperDispatchCommand : getDispatchCommand;
1388
1588
  return BUILTIN_HOOK_SPECS.map((spec) => ({
1389
1589
  source: "builtin",
@@ -1427,7 +1627,7 @@ var init_builtin_hooks = __esm({
1427
1627
  });
1428
1628
 
1429
1629
  // src/resources/base.ts
1430
- import path6 from "path";
1630
+ import path7 from "path";
1431
1631
  function toolInstallRoot(toolPath) {
1432
1632
  const segments = toolPath.split("/");
1433
1633
  if (segments[0] === ".config" && segments.length > 1) {
@@ -1451,7 +1651,7 @@ var init_base = __esm({
1451
1651
  */
1452
1652
  static async isToolInstalled(toolPath, baseDir) {
1453
1653
  const base = baseDir ?? getUserHome();
1454
- const toolRoot = path6.join(base, toolInstallRoot(toolPath));
1654
+ const toolRoot = path7.join(base, toolInstallRoot(toolPath));
1455
1655
  return pathExists(toolRoot);
1456
1656
  }
1457
1657
  /**
@@ -1459,7 +1659,7 @@ var init_base = __esm({
1459
1659
  * Returns a Set of resource names that have been explicitly deleted.
1460
1660
  */
1461
1661
  async readTombstones(localConfig) {
1462
- const tombstonePath = path6.join(localConfig.repo.localPath, this.type, TOMBSTONE_FILE);
1662
+ const tombstonePath = path7.join(localConfig.repo.localPath, this.type, TOMBSTONE_FILE);
1463
1663
  const content = await readFileSafe(tombstonePath);
1464
1664
  if (!content) return /* @__PURE__ */ new Set();
1465
1665
  return new Set(
@@ -1470,9 +1670,9 @@ var init_base = __esm({
1470
1670
  * Append a resource name to the tombstone file, deduplicating and sorting.
1471
1671
  */
1472
1672
  async addTombstone(name, localConfig) {
1473
- const dir = path6.join(localConfig.repo.localPath, this.type);
1673
+ const dir = path7.join(localConfig.repo.localPath, this.type);
1474
1674
  await ensureDir(dir);
1475
- const tombstonePath = path6.join(dir, TOMBSTONE_FILE);
1675
+ const tombstonePath = path7.join(dir, TOMBSTONE_FILE);
1476
1676
  const existing = await this.readTombstones(localConfig);
1477
1677
  existing.add(name);
1478
1678
  const sorted = [...existing].sort();
@@ -1495,17 +1695,17 @@ var init_base = __esm({
1495
1695
  });
1496
1696
 
1497
1697
  // src/resources/hooks.ts
1498
- import path7 from "path";
1499
- import { z as z2 } from "zod";
1500
- import YAML from "yaml";
1698
+ import path8 from "path";
1699
+ import { z as z3 } from "zod";
1700
+ import YAML2 from "yaml";
1501
1701
  function teamHooksYamlPath(repoPath) {
1502
- return path7.join(repoPath, "hooks", "hooks.yaml");
1702
+ return path8.join(repoPath, "hooks", "hooks.yaml");
1503
1703
  }
1504
1704
  async function parseHooksYaml(repoPath) {
1505
1705
  const content = await readFileSafe(teamHooksYamlPath(repoPath));
1506
1706
  if (!content) return null;
1507
1707
  try {
1508
- return HooksYamlSchema.parse(YAML.parse(content));
1708
+ return HooksYamlSchema.parse(YAML2.parse(content));
1509
1709
  } catch (e) {
1510
1710
  log.warn(`Invalid hooks.yaml format: ${e.message} \u2014 skipping team hooks this run`);
1511
1711
  return null;
@@ -1520,7 +1720,8 @@ function teamHookToDef(h) {
1520
1720
  command: h.command,
1521
1721
  timeout: h.timeout,
1522
1722
  description: `${TEAMAI_CUSTOM_HOOK_PREFIX}${h.id}] ${h.description}`,
1523
- tools: h.tools
1723
+ tools: h.tools,
1724
+ roles: h.roles
1524
1725
  };
1525
1726
  }
1526
1727
  async function parseTeamHooks(repoPath) {
@@ -1544,6 +1745,8 @@ async function resolveTeamHooks(teamConfig, repoPath, opts = {}) {
1544
1745
  if (defs.length > 0) log.warn(`Team hooks disabled (TEAMAI_HOOKS_DISABLED) \u2014 skipping ${defs.length} team hook(s)`);
1545
1746
  return { defs: [], builtin };
1546
1747
  }
1748
+ await warnUnknownRoleIds(repoPath, "hooks.yaml", defs.map((d) => ({ kind: "hook", name: d.key, roles: d.roles })));
1749
+ defs = defs.filter((d) => matchesRoles(d.roles, opts.activeRoles));
1547
1750
  if (sharing.requireTeamScripts) {
1548
1751
  const before = defs.length;
1549
1752
  defs = defs.filter((d) => isTeamScriptCommand(d.command));
@@ -1570,28 +1773,31 @@ var init_hooks = __esm({
1570
1773
  init_types();
1571
1774
  init_fs();
1572
1775
  init_logger();
1573
- TeamHookSchema = z2.object({
1776
+ init_roles();
1777
+ TeamHookSchema = z3.object({
1574
1778
  /** Unique id (marker + manifest index). */
1575
- id: z2.string().regex(/^[a-z0-9-]+$/),
1779
+ id: z3.string().regex(/^[a-z0-9-]+$/),
1576
1780
  /** Written into the hook description. */
1577
- description: z2.string(),
1781
+ description: z3.string(),
1578
1782
  /** Claude PascalCase event name. */
1579
- event: z2.string().min(1),
1783
+ event: z3.string().min(1),
1580
1784
  /** Optional tool matcher (e.g. "Bash"). */
1581
- matcher: z2.string().optional(),
1785
+ matcher: z3.string().optional(),
1582
1786
  /** Shell command to run. */
1583
- command: z2.string().min(1),
1787
+ command: z3.string().min(1),
1584
1788
  /** Optional per-hook timeout in seconds. */
1585
- timeout: z2.number().optional(),
1789
+ timeout: z3.number().optional(),
1586
1790
  /** Optional restriction to specific tools (default = all hook-capable tools). */
1587
- tools: z2.array(z2.string()).optional()
1791
+ tools: z3.array(z3.string()).optional(),
1792
+ /** Optional restriction to members holding one of these role ids (default = every member). */
1793
+ roles: z3.array(z3.string()).optional()
1588
1794
  });
1589
- BuiltinOverrideSchema = z2.object({
1590
- disabled: z2.array(z2.string()).default([]),
1591
- overrides: z2.record(z2.string(), z2.object({ timeout: z2.number().optional() })).default({})
1795
+ BuiltinOverrideSchema = z3.object({
1796
+ disabled: z3.array(z3.string()).default([]),
1797
+ overrides: z3.record(z3.string(), z3.object({ timeout: z3.number().optional() })).default({})
1592
1798
  }).default({ disabled: [], overrides: {} });
1593
- HooksYamlSchema = z2.object({
1594
- hooks: z2.array(TeamHookSchema).default([]),
1799
+ HooksYamlSchema = z3.object({
1800
+ hooks: z3.array(TeamHookSchema).default([]),
1595
1801
  builtin: BuiltinOverrideSchema
1596
1802
  });
1597
1803
  HooksHandler = class extends ResourceHandler {
@@ -1635,10 +1841,10 @@ __export(opencode_hooks_exports, {
1635
1841
  removeOpencodeHooks: () => removeOpencodeHooks,
1636
1842
  resolveOpencodePluginDir: () => resolveOpencodePluginDir
1637
1843
  });
1638
- import path8 from "path";
1844
+ import path9 from "path";
1639
1845
  function resolveOpencodePluginDir(baseDir, scope) {
1640
- const configDir = scope === "project" ? ".opencode" : path8.join(".config", "opencode");
1641
- return path8.join(baseDir, configDir, OPENCODE_PLUGIN_DIR);
1846
+ const configDir = scope === "project" ? ".opencode" : path9.join(".config", "opencode");
1847
+ return path9.join(baseDir, configDir, OPENCODE_PLUGIN_DIR);
1642
1848
  }
1643
1849
  function buildPluginSource() {
1644
1850
  return `// ${TEAMAI_MARKER} hooks plugin \u2014 generated by teamai, do not edit by hand.
@@ -1720,20 +1926,20 @@ export const TeamaiHooks = async ({ $, directory, worktree }) => {
1720
1926
  async function injectOpencodeHooks(baseDir, scope) {
1721
1927
  const dir = resolveOpencodePluginDir(baseDir, scope);
1722
1928
  await ensureDir(dir);
1723
- const file = path8.join(dir, OPENCODE_HOOK_FILE);
1929
+ const file = path9.join(dir, OPENCODE_HOOK_FILE);
1724
1930
  await writeFile(file, buildPluginSource());
1725
1931
  log.success(`Injected teamai OpenCode hook into ${file}`);
1726
1932
  }
1727
1933
  async function removeOpencodeHooks(baseDir, scope) {
1728
1934
  const dir = resolveOpencodePluginDir(baseDir, scope);
1729
- const file = path8.join(dir, OPENCODE_HOOK_FILE);
1935
+ const file = path9.join(dir, OPENCODE_HOOK_FILE);
1730
1936
  if (await pathExists(file)) {
1731
1937
  await remove(file);
1732
1938
  log.success(`Removed teamai OpenCode hook from ${file}`);
1733
1939
  }
1734
1940
  }
1735
1941
  function assertSafeSlug(slug) {
1736
- if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..") || path8.isAbsolute(slug)) {
1942
+ if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..") || path9.isAbsolute(slug)) {
1737
1943
  throw new Error(`Invalid agent-hook slug: ${slug}`);
1738
1944
  }
1739
1945
  }
@@ -1785,14 +1991,14 @@ async function applyOpencodeAgentHook(def) {
1785
1991
  }
1786
1992
  const dir = resolveOpencodePluginDir(def.baseDir, def.scope);
1787
1993
  await ensureDir(dir);
1788
- const file = path8.join(dir, `teamai-agent-${def.slug}.ts`);
1994
+ const file = path9.join(dir, `teamai-agent-${def.slug}.ts`);
1789
1995
  await writeFile(file, buildAgentHookPluginSource(def.slug, ocEvent, def.command, def.matcher));
1790
1996
  log.success(`Installed OpenCode agent hook [${def.slug}] in ${file}`);
1791
1997
  }
1792
1998
  async function removeOpencodeAgentHook(opts) {
1793
1999
  assertSafeSlug(opts.slug);
1794
2000
  const dir = resolveOpencodePluginDir(opts.baseDir, opts.scope);
1795
- const file = path8.join(dir, `teamai-agent-${opts.slug}.ts`);
2001
+ const file = path9.join(dir, `teamai-agent-${opts.slug}.ts`);
1796
2002
  if (await pathExists(file)) {
1797
2003
  await remove(file);
1798
2004
  log.success(`Removed OpenCode agent hook [${opts.slug}] from ${file}`);
@@ -1828,13 +2034,13 @@ __export(openclaw_hooks_exports, {
1828
2034
  resolveOpenClawHooksDir: () => resolveOpenClawHooksDir,
1829
2035
  resolveOpenclawWorkspaceDir: () => resolveOpenclawWorkspaceDir
1830
2036
  });
1831
- import path9 from "path";
2037
+ import path10 from "path";
1832
2038
  function resolveOpenClawHooksDir(tool) {
1833
2039
  if (tool === "openclaw" && process.env.OPENCLAW_STATE_DIR) {
1834
- return path9.join(process.env.OPENCLAW_STATE_DIR, "hooks");
2040
+ return path10.join(process.env.OPENCLAW_STATE_DIR, "hooks");
1835
2041
  }
1836
2042
  const home = getUserHome();
1837
- return path9.join(home, `.${tool}`, "hooks");
2043
+ return path10.join(home, `.${tool}`, "hooks");
1838
2044
  }
1839
2045
  function buildHookMd(tool) {
1840
2046
  const events = Object.keys(EVENT_MAP);
@@ -1880,21 +2086,21 @@ async function injectOpenClawHooks(workspacePath, tool = "openclaw") {
1880
2086
  log.debug(`openclaw: skip hook injection for ${tool} \u2014 workspace dir not found`);
1881
2087
  return;
1882
2088
  }
1883
- const dir = path9.join(wsDir, "hooks", OPENCLAW_HOOK_DIR);
2089
+ const dir = path10.join(wsDir, "hooks", OPENCLAW_HOOK_DIR);
1884
2090
  await ensureDir(dir);
1885
- await writeFile(path9.join(dir, "HOOK.md"), buildHookMd(tool));
1886
- await writeFile(path9.join(dir, "handler.ts"), buildHandlerTs(tool));
2091
+ await writeFile(path10.join(dir, "HOOK.md"), buildHookMd(tool));
2092
+ await writeFile(path10.join(dir, "handler.ts"), buildHandlerTs(tool));
1887
2093
  log.success(`Injected teamai OpenClaw hook into ${dir}`);
1888
2094
  await enableOpenClawInternalHooks(tool);
1889
2095
  }
1890
2096
  async function enableOpenClawInternalHooks(tool = "openclaw") {
1891
2097
  if (tool !== "openclaw") return;
1892
2098
  const stateDir = process.env.OPENCLAW_STATE_DIR;
1893
- if (!stateDir || !path9.isAbsolute(stateDir)) {
2099
+ if (!stateDir || !path10.isAbsolute(stateDir)) {
1894
2100
  log.debug("openclaw: skip enabling internal hooks \u2014 OPENCLAW_STATE_DIR unset or not absolute");
1895
2101
  return;
1896
2102
  }
1897
- const cfgPath = path9.join(stateDir, "openclaw.json");
2103
+ const cfgPath = path10.join(stateDir, "openclaw.json");
1898
2104
  try {
1899
2105
  const cfg = await readJson(cfgPath) ?? {};
1900
2106
  const hooksVal = cfg.hooks;
@@ -1915,13 +2121,13 @@ async function enableOpenClawInternalHooks(tool = "openclaw") {
1915
2121
  }
1916
2122
  }
1917
2123
  async function removeOpenClawHooks(hooksDir) {
1918
- const dir = path9.join(hooksDir, OPENCLAW_HOOK_DIR);
2124
+ const dir = path10.join(hooksDir, OPENCLAW_HOOK_DIR);
1919
2125
  if (await pathExists(dir)) {
1920
2126
  await remove(dir);
1921
2127
  log.success(`Removed teamai OpenClaw hook from ${dir}`);
1922
2128
  }
1923
2129
  if (process.env.OPENCLAW_STATE_DIR) {
1924
- const altDir = path9.join(process.env.OPENCLAW_STATE_DIR, "hooks", OPENCLAW_HOOK_DIR);
2130
+ const altDir = path10.join(process.env.OPENCLAW_STATE_DIR, "hooks", OPENCLAW_HOOK_DIR);
1925
2131
  if (altDir !== dir && await pathExists(altDir)) {
1926
2132
  await remove(altDir);
1927
2133
  log.success(`Removed teamai OpenClaw hook from ${altDir}`);
@@ -1970,16 +2176,16 @@ async function applyOpenClawAgentHook(def) {
1970
2176
  }
1971
2177
  const tool = def.tool ?? "openclaw";
1972
2178
  const hooksDir = resolveOpenClawHooksDir(tool);
1973
- const dir = path9.join(hooksDir, def.slug);
2179
+ const dir = path10.join(hooksDir, def.slug);
1974
2180
  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));
2181
+ await writeFile(path10.join(dir, "HOOK.md"), buildAgentHookMd(def.slug, openclawEvent));
2182
+ await writeFile(path10.join(dir, "handler.ts"), buildAgentHandlerTs(def.command, def.timeout ?? 10));
1977
2183
  log.success(`Installed OpenClaw agent hook [${def.slug}] in ${dir}`);
1978
2184
  }
1979
2185
  async function removeOpenClawAgentHook(opts) {
1980
2186
  const tool = opts.tool ?? "openclaw";
1981
2187
  const hooksDir = resolveOpenClawHooksDir(tool);
1982
- const dir = path9.join(hooksDir, opts.slug);
2188
+ const dir = path10.join(hooksDir, opts.slug);
1983
2189
  if (await pathExists(dir)) {
1984
2190
  await remove(dir);
1985
2191
  log.success(`Removed OpenClaw agent hook [${opts.slug}] from ${dir}`);
@@ -1989,8 +2195,8 @@ async function resolveOpenclawWorkspaceDir(workspacePath) {
1989
2195
  const candidates = [];
1990
2196
  if (workspacePath) candidates.push(workspacePath);
1991
2197
  const stateDir = process.env.OPENCLAW_STATE_DIR;
1992
- if (stateDir && path9.isAbsolute(stateDir)) {
1993
- const cfgRaw = await readFileSafe(path9.join(stateDir, "openclaw.json"));
2198
+ if (stateDir && path10.isAbsolute(stateDir)) {
2199
+ const cfgRaw = await readFileSafe(path10.join(stateDir, "openclaw.json"));
1994
2200
  if (cfgRaw) {
1995
2201
  try {
1996
2202
  const cfg = JSON.parse(cfgRaw);
@@ -2001,7 +2207,7 @@ async function resolveOpenclawWorkspaceDir(workspacePath) {
2001
2207
  }
2002
2208
  }
2003
2209
  }
2004
- candidates.push(path9.join(getUserHome(), ".openclaw", "workspace"));
2210
+ candidates.push(path10.join(getUserHome(), ".openclaw", "workspace"));
2005
2211
  for (const candidate of candidates) {
2006
2212
  if (await pathExists(candidate)) {
2007
2213
  log.debug(`openclaw: resolved workspace dir to ${candidate}`);
@@ -2036,14 +2242,14 @@ var hermes_home_exports = {};
2036
2242
  __export(hermes_home_exports, {
2037
2243
  getHermesHome: () => getHermesHome
2038
2244
  });
2039
- import path10 from "path";
2245
+ import path11 from "path";
2040
2246
  import { homedir } from "os";
2041
2247
  function getHermesHome() {
2042
2248
  const fromEnv = process.env.HERMES_HOME;
2043
2249
  if (fromEnv && fromEnv.trim() !== "") {
2044
- return path10.resolve(fromEnv);
2250
+ return path11.resolve(fromEnv);
2045
2251
  }
2046
- return path10.join(homedir(), ".hermes");
2252
+ return path11.join(homedir(), ".hermes");
2047
2253
  }
2048
2254
  var init_hermes_home = __esm({
2049
2255
  "src/hermes-home.ts"() {
@@ -2064,20 +2270,20 @@ __export(hermes_config_exports, {
2064
2270
  upsertHermesHook: () => upsertHermesHook,
2065
2271
  upsertSoulRules: () => upsertSoulRules
2066
2272
  });
2067
- import YAML2 from "yaml";
2068
- import path11 from "path";
2273
+ import YAML3 from "yaml";
2274
+ import path12 from "path";
2069
2275
  function getHermesConfigPath() {
2070
- return path11.join(getHermesHome(), "config.yaml");
2276
+ return path12.join(getHermesHome(), "config.yaml");
2071
2277
  }
2072
2278
  function getHermesSoulPath() {
2073
- return path11.join(getHermesHome(), "SOUL.md");
2279
+ return path12.join(getHermesHome(), "SOUL.md");
2074
2280
  }
2075
2281
  async function readConfigDoc() {
2076
2282
  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({});
2283
+ if (!content || content.trim() === "") return new YAML3.Document({});
2284
+ const doc = YAML3.parseDocument(content);
2285
+ if (!doc.contents || !YAML3.isMap(doc.contents)) {
2286
+ return new YAML3.Document({});
2081
2287
  }
2082
2288
  return doc;
2083
2289
  }
@@ -2150,12 +2356,12 @@ async function removeSoulRules() {
2150
2356
  await upsertSoulRules("");
2151
2357
  }
2152
2358
  function getHermesAllowlistPath() {
2153
- return path11.join(getHermesHome(), "shell-hooks-allowlist.json");
2359
+ return path12.join(getHermesHome(), "shell-hooks-allowlist.json");
2154
2360
  }
2155
2361
  async function upsertHermesHook(event, entry) {
2156
2362
  const doc = await readConfigDoc();
2157
2363
  const rawSeq = doc.getIn(["hooks", event]);
2158
- const currentJs = YAML2.isSeq(rawSeq) ? rawSeq.toJSON() : rawSeq;
2364
+ const currentJs = YAML3.isSeq(rawSeq) ? rawSeq.toJSON() : rawSeq;
2159
2365
  const arr = Array.isArray(currentJs) ? currentJs : [];
2160
2366
  const untouched = arr.filter((e) => e && typeof e === "object" && e.command !== entry.command);
2161
2367
  const cleanEntry = { command: entry.command };
@@ -2169,7 +2375,7 @@ async function upsertHermesHook(event, entry) {
2169
2375
  async function removeHermesHookByCommand(command) {
2170
2376
  const doc = await readConfigDoc();
2171
2377
  const rawHooks = doc.getIn(["hooks"]);
2172
- const hooks = YAML2.isMap(rawHooks) ? rawHooks.toJSON() : null;
2378
+ const hooks = YAML3.isMap(rawHooks) ? rawHooks.toJSON() : null;
2173
2379
  if (!hooks || typeof hooks !== "object") return;
2174
2380
  let changed = false;
2175
2381
  for (const event of Object.keys(hooks)) {
@@ -2186,7 +2392,7 @@ async function removeHermesHookByCommand(command) {
2186
2392
  }
2187
2393
  if (!changed) return;
2188
2394
  const remainingRaw = doc.getIn(["hooks"]);
2189
- const remaining = YAML2.isMap(remainingRaw) ? remainingRaw.toJSON() : null;
2395
+ const remaining = YAML3.isMap(remainingRaw) ? remainingRaw.toJSON() : null;
2190
2396
  if (!remaining || Object.keys(remaining).length === 0) {
2191
2397
  doc.deleteIn(["hooks"]);
2192
2398
  }
@@ -2231,10 +2437,10 @@ __export(hermes_hooks_exports, {
2231
2437
  removeHermesAgentHook: () => removeHermesAgentHook,
2232
2438
  removeHermesHooks: () => removeHermesHooks
2233
2439
  });
2234
- import path12 from "path";
2440
+ import path13 from "path";
2235
2441
  import { chmod } from "fs/promises";
2236
2442
  function getReportScriptPath() {
2237
- return path12.join(getHermesHome(), "hooks", "teamai-status-report.sh");
2443
+ return path13.join(getHermesHome(), "hooks", "teamai-status-report.sh");
2238
2444
  }
2239
2445
  function buildReportScript() {
2240
2446
  return [
@@ -2246,7 +2452,7 @@ function buildReportScript() {
2246
2452
  }
2247
2453
  async function injectHermesHooks() {
2248
2454
  const scriptPath = getReportScriptPath();
2249
- await ensureDir(path12.dirname(scriptPath));
2455
+ await ensureDir(path13.dirname(scriptPath));
2250
2456
  await writeFile(scriptPath, buildReportScript());
2251
2457
  try {
2252
2458
  await chmod(scriptPath, 493);
@@ -2321,7 +2527,7 @@ __export(hooks_exports, {
2321
2527
  removeHooks: () => removeHooks,
2322
2528
  sweepLegacyProjectHooks: () => sweepLegacyProjectHooks
2323
2529
  });
2324
- import path13 from "path";
2530
+ import path14 from "path";
2325
2531
  import { realpathSync } from "fs";
2326
2532
  function detectFormat(tool) {
2327
2533
  if (CODEX_TOOLS.has(tool)) return "codex";
@@ -2354,7 +2560,7 @@ function canonicalProjectRoot(projectRoot) {
2354
2560
  try {
2355
2561
  return realpathSync.native(projectRoot);
2356
2562
  } catch {
2357
- return path13.resolve(projectRoot);
2563
+ return path14.resolve(projectRoot);
2358
2564
  }
2359
2565
  }
2360
2566
  function gateTeamHookCommand(command, projectRoot) {
@@ -2414,16 +2620,30 @@ function toCodexEntry(def) {
2414
2620
  return entry;
2415
2621
  }
2416
2622
  function toZcodeEntry(def) {
2417
- const entry = {
2623
+ const ZCODE_TIMEOUT_MS = {
2624
+ SessionStart: 18e4,
2625
+ Stop: 6e4,
2626
+ PostToolUse: 3e4,
2627
+ UserPromptSubmit: 6e4
2628
+ };
2629
+ const entry = process.platform === "win32" ? {
2630
+ // Windows must NOT spawn bare `bash`: CreateProcess resolves it to
2631
+ // System32's WSL launcher before any PATH directory, and the WSL side
2632
+ // has a different $HOME (no ~/.teamai state) and often no Node ≥ 20.
2633
+ // cmd.exe is always present in System32 and resolves teamai from the
2634
+ // Windows PATH (the npm shim is a .cmd, so a shell is required).
2635
+ type: "process",
2636
+ command: "cmd",
2637
+ args: ["/c", def.command],
2638
+ timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 6e4
2639
+ } : {
2418
2640
  type: "process",
2419
2641
  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.
2642
+ // Stored verbatim: the shell payload must equal `def.command` exactly
2643
+ // so managed-entry detection and the managed-hooks manifest share one
2644
+ // command representation (the same invariant the Codex format keeps).
2425
2645
  args: ["-lc", def.command],
2426
- ...def.timeout !== void 0 ? { timeoutMs: def.timeout * 1e3 } : {}
2646
+ timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 6e4
2427
2647
  };
2428
2648
  const group = { hooks: [entry] };
2429
2649
  if (def.matcher && def.matcher !== "*") group.matcher = def.matcher;
@@ -2431,7 +2651,7 @@ function toZcodeEntry(def) {
2431
2651
  }
2432
2652
  function zcodeEntryCommand(entry) {
2433
2653
  const hook = entry.hooks?.[0];
2434
- if (hook?.command === "bash" && hook.args?.[0] === "-lc") return hook.args[1] ?? "";
2654
+ if (Array.isArray(hook?.args) && hook.args.length > 1) return hook.args[1] ?? "";
2435
2655
  return hook?.command ?? "";
2436
2656
  }
2437
2657
  function desiredEventOrder(defs, mapEvent) {
@@ -2465,7 +2685,7 @@ async function reconcileClaudeFormat(settingsPath, tool, teamDefs, opts, teamAct
2465
2685
  return true;
2466
2686
  };
2467
2687
  const expanded = expandHome(settingsPath);
2468
- await ensureDir(path13.dirname(expanded));
2688
+ await ensureDir(path14.dirname(expanded));
2469
2689
  const settings = await readJson(expanded) ?? {};
2470
2690
  if (!settings.hooks) settings.hooks = {};
2471
2691
  let changed = false;
@@ -2497,7 +2717,7 @@ async function reconcileClaudeFormat(settingsPath, tool, teamDefs, opts, teamAct
2497
2717
  }
2498
2718
  async function reconcileCursorFormat(hooksPath, tool, teamDefs, opts, priorTeamCommands) {
2499
2719
  const expanded = expandHome(hooksPath);
2500
- await ensureDir(path13.dirname(expanded));
2720
+ await ensureDir(path14.dirname(expanded));
2501
2721
  const hooksJson = await readJson(expanded) ?? { version: 1, hooks: {} };
2502
2722
  if (!hooksJson.version) hooksJson.version = 1;
2503
2723
  if (!hooksJson.hooks) hooksJson.hooks = {};
@@ -2545,7 +2765,7 @@ async function reconcileCursorFormat(hooksPath, tool, teamDefs, opts, priorTeamC
2545
2765
  }
2546
2766
  async function reconcileCodexFormat(hooksPath, tool, teamDefs, opts, priorTeamCommands) {
2547
2767
  const expanded = expandHome(hooksPath);
2548
- await ensureDir(path13.dirname(expanded));
2768
+ await ensureDir(path14.dirname(expanded));
2549
2769
  const hooksJson = await readJson(expanded) ?? {};
2550
2770
  if (!hooksJson.hooks) hooksJson.hooks = {};
2551
2771
  const isManaged = (entry) => {
@@ -2575,10 +2795,15 @@ async function reconcileCodexFormat(hooksPath, tool, teamDefs, opts, priorTeamCo
2575
2795
  }
2576
2796
  async function reconcileZcodeFormat(settingsPath, tool, teamDefs, opts, priorTeamCommands) {
2577
2797
  const expanded = expandHome(settingsPath);
2578
- await ensureDir(path13.dirname(expanded));
2798
+ await ensureDir(path14.dirname(expanded));
2579
2799
  const cfg = await readJson(expanded) ?? {};
2580
2800
  if (!cfg.hooks) cfg.hooks = {};
2581
2801
  let changed = false;
2802
+ const unknownHookKeys = Object.keys(cfg.hooks).filter((k) => k !== "enabled" && k !== "events");
2803
+ if (unknownHookKeys.length > 0) {
2804
+ for (const k of unknownHookKeys) delete cfg.hooks[k];
2805
+ changed = true;
2806
+ }
2582
2807
  if (!opts.removeAll && cfg.hooks.enabled !== true) {
2583
2808
  cfg.hooks.enabled = true;
2584
2809
  changed = true;
@@ -2628,7 +2853,7 @@ function isAgentClaudeEntry(entry, slug) {
2628
2853
  async function applyAgentHook(settingsPath, tool, def) {
2629
2854
  const format = detectFormat(tool);
2630
2855
  const expanded = expandHome(settingsPath);
2631
- await ensureDir(path13.dirname(expanded));
2856
+ await ensureDir(path14.dirname(expanded));
2632
2857
  const hookDef = {
2633
2858
  source: "team",
2634
2859
  key: def.slug,
@@ -2854,15 +3079,15 @@ async function hasTeamaiHooks(settingsPath, tool, manifestPath) {
2854
3079
  async function reconcileOpencodePlugin(baseDir, removeAll = false, installedBaseDir) {
2855
3080
  const home = getUserHome();
2856
3081
  const { injectOpencodeHooks: injectOpencodeHooks2, removeOpencodeHooks: removeOpencodeHooks2 } = await Promise.resolve().then(() => (init_opencode_hooks(), opencode_hooks_exports));
2857
- if (path13.resolve(baseDir) !== path13.resolve(home)) {
3082
+ if (path14.resolve(baseDir) !== path14.resolve(home)) {
2858
3083
  await removeOpencodeHooks2(baseDir, "project");
2859
3084
  }
2860
3085
  if (removeAll) {
2861
3086
  await removeOpencodeHooks2(home, "user");
2862
3087
  return;
2863
3088
  }
2864
- const homeInstalled = await pathExists(path13.join(home, ".config", "opencode"));
2865
- const projectInstalled = installedBaseDir ? await pathExists(path13.join(installedBaseDir, ".opencode")) : false;
3089
+ const homeInstalled = await pathExists(path14.join(home, ".config", "opencode"));
3090
+ const projectInstalled = installedBaseDir ? await pathExists(path14.join(installedBaseDir, ".opencode")) : false;
2866
3091
  if (homeInstalled || projectInstalled) {
2867
3092
  await injectOpencodeHooks2(home, "user");
2868
3093
  }
@@ -2876,9 +3101,9 @@ async function injectHooksToAllTools(toolPaths, baseDir, filterAgents2) {
2876
3101
  if (filterAgents2 && !filterAgents2.includes(tool)) continue;
2877
3102
  if (skipped.has(tool)) continue;
2878
3103
  if (paths.settings) {
2879
- const toolRoot = path13.join(resolvedBaseDir, paths.settings.split("/")[0]);
3104
+ const toolRoot = path14.join(resolvedBaseDir, paths.settings.split("/")[0]);
2880
3105
  if (!await pathExists(toolRoot)) continue;
2881
- const settingsPath = path13.join(resolvedBaseDir, paths.settings);
3106
+ const settingsPath = path14.join(resolvedBaseDir, paths.settings);
2882
3107
  try {
2883
3108
  await injectHooks(settingsPath, tool);
2884
3109
  } catch (e) {
@@ -2941,10 +3166,10 @@ async function reconcileHooksToAllTools(toolPaths, baseDir, teamDefs, manifestPa
2941
3166
  continue;
2942
3167
  }
2943
3168
  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;
3169
+ const toolRoot = path14.join(baseDir, paths.settings.split("/")[0]);
3170
+ const installedRoot = opts.installedBaseDir ? path14.join(opts.installedBaseDir, paths.settings.split("/")[0]) : toolRoot;
2946
3171
  if (!await pathExists(toolRoot) && !await pathExists(installedRoot)) continue;
2947
- const settingsPath = path13.join(baseDir, paths.settings);
3172
+ const settingsPath = path14.join(baseDir, paths.settings);
2948
3173
  try {
2949
3174
  await reconcileHooks(settingsPath, tool, teamDefs, {
2950
3175
  manifestPath,
@@ -2960,7 +3185,7 @@ async function reconcileHooksToAllTools(toolPaths, baseDir, teamDefs, manifestPa
2960
3185
  async function hasInstalledCodexTrustGatedTool(toolPaths, baseDir) {
2961
3186
  for (const [tool, paths] of Object.entries(toolPaths)) {
2962
3187
  if (!isCodexTrustGatedTool(tool) || !paths.settings) continue;
2963
- const toolRoot = path13.join(baseDir, paths.settings.split("/")[0]);
3188
+ const toolRoot = path14.join(baseDir, paths.settings.split("/")[0]);
2964
3189
  if (await pathExists(toolRoot)) return true;
2965
3190
  }
2966
3191
  return false;
@@ -2982,7 +3207,11 @@ async function sweepLegacyProjectHooks(toolPaths, localConfig) {
2982
3207
  }
2983
3208
  }
2984
3209
  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 });
3210
+ const { defs: teamDefs, builtin } = opts.removeAll ? { defs: [], builtin: void 0 } : await resolveTeamHooks(teamConfig, localConfig.repo.localPath, {
3211
+ auto: opts.auto,
3212
+ silent: opts.silent,
3213
+ activeRoles: activeRoleIds(localConfig)
3214
+ });
2986
3215
  const { baseDir, manifestPath } = resolveHookScope(localConfig);
2987
3216
  let filterAgents2 = opts.filterAgents ?? localConfig.enabledAgents;
2988
3217
  const disabled = localConfig.disabledAgents;
@@ -3008,6 +3237,7 @@ var init_hooks2 = __esm({
3008
3237
  init_logger();
3009
3238
  init_types();
3010
3239
  init_types();
3240
+ init_roles();
3011
3241
  init_builtin_hooks();
3012
3242
  init_hooks();
3013
3243
  init_home();
@@ -3248,7 +3478,7 @@ __export(builtin_rules_exports, {
3248
3478
  LEGACY_RULE_NAMES: () => LEGACY_RULE_NAMES,
3249
3479
  deployBuiltinRules: () => deployBuiltinRules
3250
3480
  });
3251
- import path14 from "path";
3481
+ import path15 from "path";
3252
3482
  import fs4 from "fs/promises";
3253
3483
  async function deployBuiltinRules(teamConfig, localConfig, options) {
3254
3484
  const baseDir = localConfig ? resolveBaseDir(localConfig) : getUserHome();
@@ -3263,19 +3493,19 @@ async function deployBuiltinRules(teamConfig, localConfig, options) {
3263
3493
  continue;
3264
3494
  }
3265
3495
  if (localConfig && isAgentExcluded(localConfig, tool)) continue;
3266
- const rulesDir = path14.join(baseDir, toolPath.rules);
3496
+ const rulesDir = path15.join(baseDir, toolPath.rules);
3267
3497
  if (!await pathExists(rulesDir)) continue;
3268
3498
  try {
3269
3499
  await ensureDir(rulesDir);
3270
3500
  const ext = ruleFileExtensionForTool(tool);
3271
3501
  for (const rule of builtinRules) {
3272
- const destFile = path14.join(rulesDir, `${rule.name}${ext}`);
3502
+ const destFile = path15.join(rulesDir, `${rule.name}${ext}`);
3273
3503
  const content = usesCursorMdcRules(tool) ? teamRuleToCursorMdc(rule.content) : rule.content;
3274
3504
  await writeFile(destFile, content);
3275
3505
  log.debug(`Deployed built-in rule ${rule.name} \u2192 ${tool}`);
3276
3506
  if (ext !== ".md") {
3277
3507
  try {
3278
- await fs4.unlink(path14.join(rulesDir, `${rule.name}.md`));
3508
+ await fs4.unlink(path15.join(rulesDir, `${rule.name}.md`));
3279
3509
  log.debug(`Removed legacy .md built-in rule ${rule.name} from ${tool}`);
3280
3510
  } catch {
3281
3511
  }
@@ -3283,7 +3513,7 @@ async function deployBuiltinRules(teamConfig, localConfig, options) {
3283
3513
  }
3284
3514
  for (const legacyName of LEGACY_RULE_NAMES) {
3285
3515
  for (const legacyExt of /* @__PURE__ */ new Set([ext, ".md"])) {
3286
- const legacyFile = path14.join(rulesDir, `${legacyName}${legacyExt}`);
3516
+ const legacyFile = path15.join(rulesDir, `${legacyName}${legacyExt}`);
3287
3517
  try {
3288
3518
  await fs4.unlink(legacyFile);
3289
3519
  log.debug(`Removed legacy built-in rule ${legacyName} from ${tool}`);
@@ -3370,7 +3600,7 @@ teamai-recall subagent \u7684\u8FD4\u56DE\u91CC\u5DF2\u5217\u51FA\u672C\u6B21\u6
3370
3600
  });
3371
3601
 
3372
3602
  // src/utils/pre-push-sync.ts
3373
- import path15 from "path";
3603
+ import path16 from "path";
3374
3604
  async function syncTeamUpdatesToLocal(teamConfig, localConfig, lastPullRev) {
3375
3605
  if (!lastPullRev) {
3376
3606
  log.debug("No lastPullRev \u2014 skipping pre-push sync");
@@ -3382,12 +3612,12 @@ async function syncTeamUpdatesToLocal(teamConfig, localConfig, lastPullRev) {
3382
3612
  await syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, lastPullRev);
3383
3613
  }
3384
3614
  async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, lastPullRev) {
3385
- const teamRulesDir = path15.join(repoPath, "rules");
3615
+ const teamRulesDir = path16.join(repoPath, "rules");
3386
3616
  if (!await pathExists(teamRulesDir)) return;
3387
3617
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
3388
3618
  if (!toolPath.rules) continue;
3389
3619
  if (!await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
3390
- const rulesDir = path15.join(baseDir, toolPath.rules);
3620
+ const rulesDir = path16.join(baseDir, toolPath.rules);
3391
3621
  if (!await pathExists(rulesDir)) continue;
3392
3622
  const ext = ruleFileExtensionForTool(tool);
3393
3623
  const isMdcTool = usesCursorMdcRules(tool);
@@ -3396,9 +3626,9 @@ async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, last
3396
3626
  if (!file.endsWith(ext)) continue;
3397
3627
  const name = file.slice(0, -ext.length);
3398
3628
  if (EXCLUDED_RULE_NAMES.has(name)) continue;
3399
- const localFilePath = path15.join(rulesDir, file);
3629
+ const localFilePath = path16.join(rulesDir, file);
3400
3630
  const teamRelPath = `rules/${name}.md`;
3401
- const teamFilePath = path15.join(teamRulesDir, `${name}.md`);
3631
+ const teamFilePath = path16.join(teamRulesDir, `${name}.md`);
3402
3632
  if (!await pathExists(teamFilePath)) continue;
3403
3633
  if (isMdcTool) {
3404
3634
  const localRaw = await readFileSafe(localFilePath);
@@ -3424,19 +3654,19 @@ async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, last
3424
3654
  }
3425
3655
  }
3426
3656
  async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, lastPullRev) {
3427
- const teamSkillsDir = path15.join(repoPath, "skills");
3657
+ const teamSkillsDir = path16.join(repoPath, "skills");
3428
3658
  if (!await pathExists(teamSkillsDir)) return;
3429
3659
  const teamSkillPaths = /* @__PURE__ */ new Map();
3430
3660
  const topDirs = await listDirs(teamSkillsDir);
3431
3661
  for (const dir of topDirs) {
3432
- const dirPath = path15.join(teamSkillsDir, dir);
3433
- if (await pathExists(path15.join(dirPath, "SKILL.md"))) {
3662
+ const dirPath = path16.join(teamSkillsDir, dir);
3663
+ if (await pathExists(path16.join(dirPath, "SKILL.md"))) {
3434
3664
  teamSkillPaths.set(dir, dirPath);
3435
3665
  } else {
3436
3666
  const subDirs = await listDirs(dirPath);
3437
3667
  for (const subDir of subDirs) {
3438
3668
  if (!teamSkillPaths.has(subDir)) {
3439
- teamSkillPaths.set(subDir, path15.join(dirPath, subDir));
3669
+ teamSkillPaths.set(subDir, path16.join(dirPath, subDir));
3440
3670
  }
3441
3671
  }
3442
3672
  }
@@ -3445,12 +3675,12 @@ async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, las
3445
3675
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
3446
3676
  if (!toolPath.skills) continue;
3447
3677
  if (!await ResourceHandler.isToolInstalled(toolPath.skills, baseDir)) continue;
3448
- const skillsDir = path15.join(baseDir, toolPath.skills);
3678
+ const skillsDir = path16.join(baseDir, toolPath.skills);
3449
3679
  if (!await pathExists(skillsDir)) continue;
3450
3680
  const localSkillNames = await listDirs(skillsDir);
3451
3681
  for (const skillName of localSkillNames) {
3452
3682
  if (!teamSkillPaths.has(skillName)) continue;
3453
- const localSkillDir = path15.join(skillsDir, skillName);
3683
+ const localSkillDir = path16.join(skillsDir, skillName);
3454
3684
  const teamSkillDir = teamSkillPaths.get(skillName);
3455
3685
  if (await dirTeamSubsetEqual(localSkillDir, teamSkillDir, [CONTRIBUTORS_FILE3])) continue;
3456
3686
  const teamFiles = await listFilesRecursive(teamSkillDir);
@@ -3458,10 +3688,10 @@ async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, las
3458
3688
  let anyDiffers = false;
3459
3689
  for (const file of teamFiles) {
3460
3690
  if (file === CONTRIBUTORS_FILE3) continue;
3461
- const localFile = path15.join(localSkillDir, file);
3462
- const teamFile = path15.join(teamSkillDir, file);
3691
+ const localFile = path16.join(localSkillDir, file);
3692
+ const teamFile = path16.join(teamSkillDir, file);
3463
3693
  if (!await pathExists(localFile)) {
3464
- const relFromRepo2 = path15.relative(repoPath, teamFile);
3694
+ const relFromRepo2 = path16.relative(repoPath, teamFile);
3465
3695
  const oldContent2 = await getFileContentAtRev(repoPath, lastPullRev, relFromRepo2);
3466
3696
  if (oldContent2 === null) {
3467
3697
  anyDiffers = true;
@@ -3472,7 +3702,7 @@ async function syncSkillsToLocal(teamConfig, localConfig, repoPath, baseDir, las
3472
3702
  }
3473
3703
  if (await fileContentEqual(localFile, teamFile)) continue;
3474
3704
  anyDiffers = true;
3475
- const relFromRepo = path15.relative(repoPath, teamFile);
3705
+ const relFromRepo = path16.relative(repoPath, teamFile);
3476
3706
  const oldContent = await getFileContentAtRev(repoPath, lastPullRev, relFromRepo);
3477
3707
  if (oldContent === null) {
3478
3708
  allMatchOld = false;
@@ -3604,12 +3834,12 @@ var init_rest_auth = __esm({
3604
3834
  import { execSync, spawnSync } from "child_process";
3605
3835
  import fs5 from "fs";
3606
3836
  import os2 from "os";
3607
- import path16 from "path";
3837
+ import path17 from "path";
3608
3838
  function gfInstallDir() {
3609
- return path16.join(getTeamaiHomeDir(), "gf");
3839
+ return path17.join(getTeamaiHomeDir(), "gf");
3610
3840
  }
3611
3841
  function gfBinPath() {
3612
- return path16.join(gfInstallDir(), "gf", "bin", "gf");
3842
+ return path17.join(gfInstallDir(), "gf", "bin", "gf");
3613
3843
  }
3614
3844
  function shellQuote2(s) {
3615
3845
  return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -3747,7 +3977,7 @@ function ensureAuthenticated() {
3747
3977
  }
3748
3978
  function gfGetOAuthToken() {
3749
3979
  try {
3750
- const netrcPath2 = path16.join(os2.homedir(), ".netrc");
3980
+ const netrcPath2 = path17.join(os2.homedir(), ".netrc");
3751
3981
  if (!fs5.existsSync(netrcPath2)) return null;
3752
3982
  const content = fs5.readFileSync(netrcPath2, "utf-8");
3753
3983
  const match = content.match(
@@ -5599,7 +5829,7 @@ var init_repo_url4 = __esm({
5599
5829
  import { spawnSync as spawnSync5 } from "child_process";
5600
5830
  import fs6 from "fs";
5601
5831
  import os3 from "os";
5602
- import path17 from "path";
5832
+ import path18 from "path";
5603
5833
  function gitcodeApiBase() {
5604
5834
  return GITCODE_API_BASE;
5605
5835
  }
@@ -5627,7 +5857,7 @@ function authHeaders3(token) {
5627
5857
  };
5628
5858
  }
5629
5859
  function netrcPath() {
5630
- return path17.join(os3.homedir(), ".netrc");
5860
+ return path18.join(os3.homedir(), ".netrc");
5631
5861
  }
5632
5862
  function readNetrcToken() {
5633
5863
  try {
@@ -6113,12 +6343,13 @@ function buildRepoInfo5(owner, repo, remoteUrl) {
6113
6343
  projectId: encodeURIComponent(`${owner}/${repo}`)
6114
6344
  };
6115
6345
  }
6116
- function parseGenericGitRepoInput(input) {
6346
+ function parseGenericGitRepoInputWithOptions(input, options = {}) {
6117
6347
  const trimmed = input.trim();
6118
- if (/^http:\/\//i.test(trimmed)) {
6348
+ const allowExistingInsecureOrigin = options.allowExistingInsecureOrigin === true;
6349
+ if (/^http:\/\//i.test(trimmed) && !allowExistingInsecureOrigin) {
6119
6350
  throw invalidRepoUrl("plain HTTP is not supported; use HTTPS or SSH");
6120
6351
  }
6121
- if (/^https:\/\//i.test(trimmed) || /^ssh:\/\//i.test(trimmed)) {
6352
+ if (/^https?:\/\//i.test(trimmed) || /^ssh:\/\//i.test(trimmed)) {
6122
6353
  let parsed;
6123
6354
  try {
6124
6355
  parsed = new URL(trimmed);
@@ -6126,7 +6357,7 @@ function parseGenericGitRepoInput(input) {
6126
6357
  throw invalidRepoUrl();
6127
6358
  }
6128
6359
  const httpCredentials = /^https?:$/i.test(parsed.protocol) && (parsed.username || parsed.password);
6129
- if (!parsed.hostname || parsed.password || httpCredentials) {
6360
+ if (!parsed.hostname || !allowExistingInsecureOrigin && (parsed.password || httpCredentials)) {
6130
6361
  throw new Error(
6131
6362
  "Invalid Git repo URL. Do not embed credentials in the URL; configure a Git credential helper or SSH key instead."
6132
6363
  );
@@ -6135,7 +6366,7 @@ function parseGenericGitRepoInput(input) {
6135
6366
  throw invalidRepoUrl("query strings and fragments are not supported");
6136
6367
  }
6137
6368
  const { owner, repo, fullPath } = parsePath(parsed.pathname);
6138
- const auth = parsed.username ? `${parsed.username}@` : "";
6369
+ const auth = /^ssh:$/i.test(parsed.protocol) && parsed.username ? `${parsed.username}@` : "";
6139
6370
  const remoteUrl = `${parsed.protocol}//${auth}${parsed.host}/${fullPath}.git`;
6140
6371
  return buildRepoInfo5(owner, repo, remoteUrl);
6141
6372
  }
@@ -6152,6 +6383,12 @@ function parseGenericGitRepoInput(input) {
6152
6383
  }
6153
6384
  throw invalidRepoUrl();
6154
6385
  }
6386
+ function parseGenericGitRepoInput(input) {
6387
+ return parseGenericGitRepoInputWithOptions(input);
6388
+ }
6389
+ function parseGenericGitExistingRemote(input) {
6390
+ return parseGenericGitRepoInputWithOptions(input, { allowExistingInsecureOrigin: true });
6391
+ }
6155
6392
  var SUPPORTED_URL_HINT;
6156
6393
  var init_repo_url5 = __esm({
6157
6394
  "src/providers/git/repo-url.ts"() {
@@ -6527,17 +6764,17 @@ __export(builtin_skills_exports, {
6527
6764
  deployBuiltinSkills: () => deployBuiltinSkills
6528
6765
  });
6529
6766
  import fs7 from "fs";
6530
- import path18 from "path";
6767
+ import path19 from "path";
6531
6768
  import { fileURLToPath as fileURLToPath2 } from "url";
6532
6769
  import fse2 from "fs-extra";
6533
6770
  function getBuiltinSkillsDir() {
6534
- const distDir = path18.dirname(fileURLToPath2(import.meta.url));
6535
- return path18.join(distDir, "..", "skills");
6771
+ const distDir = path19.dirname(fileURLToPath2(import.meta.url));
6772
+ return path19.join(distDir, "..", "skills");
6536
6773
  }
6537
6774
  async function copyBuiltinSkillDir(srcDir, destDir) {
6538
6775
  await fse2.copy(srcDir, destDir, {
6539
6776
  overwrite: true,
6540
- filter: (srcPath) => !path18.basename(srcPath).startsWith(".")
6777
+ filter: (srcPath) => !path19.basename(srcPath).startsWith(".")
6541
6778
  });
6542
6779
  }
6543
6780
  async function deployBuiltinSkills(teamConfig, localConfig, options) {
@@ -6559,7 +6796,7 @@ async function deployBuiltinSkills(teamConfig, localConfig, options) {
6559
6796
  const skillNames = [];
6560
6797
  for (const entry of entries) {
6561
6798
  if (options?.skipRecall && RECALL_DEPENDENT_SKILLS.has(entry)) continue;
6562
- const skillMd = path18.join(builtinDir, entry, "SKILL.md");
6799
+ const skillMd = path19.join(builtinDir, entry, "SKILL.md");
6563
6800
  if (await pathExists(skillMd)) {
6564
6801
  skillNames.push(entry);
6565
6802
  }
@@ -6575,7 +6812,7 @@ async function deployBuiltinSkills(teamConfig, localConfig, options) {
6575
6812
  }
6576
6813
  if (localConfig && isAgentExcluded(localConfig, tool)) continue;
6577
6814
  for (const skillName of skillNames) {
6578
- const srcDir = path18.join(builtinDir, skillName);
6815
+ const srcDir = path19.join(builtinDir, skillName);
6579
6816
  const destDir = await resolveSkillDestination(tool, toolPath.skills, baseDir, skillName, srcDir);
6580
6817
  try {
6581
6818
  await copyBuiltinSkillDir(srcDir, destDir);
@@ -6603,142 +6840,6 @@ var init_builtin_skills = __esm({
6603
6840
  }
6604
6841
  });
6605
6842
 
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
6843
  // src/utils/path-safety.ts
6743
6844
  var path_safety_exports = {};
6744
6845
  __export(path_safety_exports, {
@@ -7487,10 +7588,20 @@ async function countInterventions(transcriptPath) {
7487
7588
  const { interrupt, toolReject, toolError } = await scanTranscriptStop(transcriptPath);
7488
7589
  return { interrupt, toolReject, toolError };
7489
7590
  }
7490
- function isCorrectionPrompt(text) {
7591
+ function wordBoundaryPattern(keyword) {
7592
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7593
+ return new RegExp(`(?<![\\p{L}\\p{N}_])${escaped}(?![\\p{L}\\p{N}_])`, "u");
7594
+ }
7595
+ function containsKeyword(lower, keyword) {
7596
+ const k = keyword.trim().normalize("NFC").toLowerCase();
7597
+ if (!k) return false;
7598
+ if (UNSPACED_SCRIPT_RE.test(k)) return lower.includes(k);
7599
+ return wordBoundaryPattern(k).test(lower);
7600
+ }
7601
+ function isCorrectionPrompt(text, extraKeywords = []) {
7491
7602
  if (!text) return false;
7492
- const lower = text.toLowerCase();
7493
- return CORRECTION_KEYWORDS.some((k) => lower.includes(k));
7603
+ const lower = text.normalize("NFC").toLowerCase();
7604
+ return [...CORRECTION_KEYWORDS, ...extraKeywords].some((k) => containsKeyword(lower, k));
7494
7605
  }
7495
7606
  function mapEventType(hookEventName) {
7496
7607
  switch (hookEventName) {
@@ -7511,7 +7622,7 @@ function mapEventType(hookEventName) {
7511
7622
  return null;
7512
7623
  }
7513
7624
  }
7514
- async function parseHookEvent(raw, tool) {
7625
+ async function parseHookEvent(raw, tool, options) {
7515
7626
  if (!raw.trim()) return null;
7516
7627
  let hookData;
7517
7628
  try {
@@ -7550,6 +7661,7 @@ async function parseHookEvent(raw, tool) {
7550
7661
  }
7551
7662
  if (eventType === "prompt_submit" && typeof hookData.prompt === "string") {
7552
7663
  event.promptSummary = hookData.prompt.slice(0, 200);
7664
+ event.correction = isCorrectionPrompt(hookData.prompt, options?.correctionKeywords);
7553
7665
  }
7554
7666
  if (eventType === "stop" && typeof hookData.transcript_path === "string") {
7555
7667
  event.transcriptPath = hookData.transcript_path;
@@ -7856,7 +7968,8 @@ function aggregateSessionMetrics(events) {
7856
7968
  const stopAt = lastStopAt.get(event.sessionId);
7857
7969
  if (stopAt !== void 0) {
7858
7970
  const gap = new Date(event.timestamp).getTime() - stopAt;
7859
- if (gap >= 0 && gap <= CORRECTION_WINDOW_MS && isCorrectionPrompt(event.promptSummary)) {
7971
+ const isCorrection = event.correction ?? isCorrectionPrompt(event.promptSummary);
7972
+ if (gap >= 0 && gap <= CORRECTION_WINDOW_MS && isCorrection) {
7860
7973
  m.correction++;
7861
7974
  }
7862
7975
  lastStopAt.delete(event.sessionId);
@@ -7918,7 +8031,7 @@ async function dashboardReport(toolArg) {
7918
8031
  compactEvents().catch(() => {
7919
8032
  });
7920
8033
  }
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;
8034
+ 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
8035
  var init_dashboard_collector = __esm({
7923
8036
  "src/dashboard-collector.ts"() {
7924
8037
  "use strict";
@@ -7941,6 +8054,7 @@ var init_dashboard_collector = __esm({
7941
8054
  CODEBUDDY_USAGE_RETRY_MS = 250;
7942
8055
  CODEBUDDY_BLOB_MAX_COUNT = 2e3;
7943
8056
  CODEBUDDY_REJECT_MARKER = "User rejected this command";
8057
+ UNSPACED_SCRIPT_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
7944
8058
  }
7945
8059
  });
7946
8060
 
@@ -8297,7 +8411,7 @@ var CLAUDE_TOOLS, CURSOR_TOOLS2, CODEX_TOOLS2, BUDDY_TOOLS, OPENCODE_TOOLS, MCP_
8297
8411
  var init_mcp_format = __esm({
8298
8412
  "src/resources/mcp-format.ts"() {
8299
8413
  "use strict";
8300
- CLAUDE_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "qoder", "zcode"]);
8414
+ CLAUDE_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "qoder", "kiro", "zcode"]);
8301
8415
  CURSOR_TOOLS2 = /* @__PURE__ */ new Set(["cursor"]);
8302
8416
  CODEX_TOOLS2 = /* @__PURE__ */ new Set(["codex", "codex-internal", "tcodex"]);
8303
8417
  BUDDY_TOOLS = /* @__PURE__ */ new Set(["codebuddy", "workbuddy"]);
@@ -8354,7 +8468,8 @@ function teamMcpToDef(s) {
8354
8468
  env: s.env,
8355
8469
  timeout: s.timeout,
8356
8470
  requires: s.requires,
8357
- tools: s.tools
8471
+ tools: s.tools,
8472
+ roles: s.roles
8358
8473
  };
8359
8474
  }
8360
8475
  async function parseTeamMcpServers(repoPath) {
@@ -8380,7 +8495,8 @@ var init_mcp = __esm({
8380
8495
  env: z4.record(z4.string(), z4.string()).optional(),
8381
8496
  timeout: z4.number().int().positive().optional(),
8382
8497
  requires: z4.array(z4.string()).optional(),
8383
- tools: z4.array(z4.string()).optional()
8498
+ tools: z4.array(z4.string()).optional(),
8499
+ roles: z4.array(z4.string()).optional()
8384
8500
  }).refine((s) => s.transport === "stdio" ? !!s.command : true, {
8385
8501
  message: "stdio transport requires `command`"
8386
8502
  }).refine((s) => s.transport === "stdio" ? true : !!s.url, {
@@ -8703,6 +8819,14 @@ async function reconcileMcpForConfig(teamConfig, localConfig, options = {}) {
8703
8819
  return { changes, wrote };
8704
8820
  }
8705
8821
  const excluded = new Set(localConfig.excludedSkills ?? []);
8822
+ const activeRoles = activeRoleIds(localConfig);
8823
+ if (!removeAll) {
8824
+ await warnUnknownRoleIds(
8825
+ localConfig.repo.localPath,
8826
+ "mcp.yaml",
8827
+ teamDefs.map((def) => ({ kind: "server", name: def.name, roles: def.roles }))
8828
+ );
8829
+ }
8706
8830
  const targets = await resolveMcpTargets(teamConfig, localConfig);
8707
8831
  if (targets.length === 0) return { changes, wrote };
8708
8832
  const dataHome = getDataHome(localConfig);
@@ -8726,6 +8850,7 @@ async function reconcileMcpForConfig(teamConfig, localConfig, options = {}) {
8726
8850
  const desired = /* @__PURE__ */ new Map();
8727
8851
  for (const raw of teamDefs) {
8728
8852
  if (raw.tools && !raw.tools.includes(target.tool)) continue;
8853
+ if (!matchesRoles(raw.roles, activeRoles)) continue;
8729
8854
  if (excluded.has(raw.name)) {
8730
8855
  changes.push({ tool: target.tool, server: raw.name, action: "skipped", reason: "excluded by user" });
8731
8856
  continue;
@@ -8878,6 +9003,7 @@ var init_mcp_reconcile = __esm({
8878
9003
  init_types();
8879
9004
  init_mcp_format();
8880
9005
  init_mcp();
9006
+ init_roles();
8881
9007
  init_fs();
8882
9008
  init_logger();
8883
9009
  init_mcp_manifest();
@@ -12991,6 +13117,8 @@ function agentFileExtensionForTool(tool) {
12991
13117
  case "codex-internal":
12992
13118
  case "tcodex":
12993
13119
  return ".toml";
13120
+ case "kiro":
13121
+ return ".json";
12994
13122
  default:
12995
13123
  return ".md";
12996
13124
  }
@@ -13051,6 +13179,35 @@ function renderForJoycode(spec) {
13051
13179
  content: renderMarkdownAgent(spec, spec.tool_extras?.["joycode"])
13052
13180
  };
13053
13181
  }
13182
+ function isRecord(value) {
13183
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13184
+ }
13185
+ function isManagedKiroSessionHook(value) {
13186
+ return isRecord(value) && typeof value["command"] === "string" && value["command"].includes("teamai hook-dispatch session-start --tool kiro");
13187
+ }
13188
+ function renderForKiro(spec) {
13189
+ const extras = { ...spec.tool_extras?.["kiro"] ?? {} };
13190
+ const existingHooks = isRecord(extras["hooks"]) ? { ...extras["hooks"] } : {};
13191
+ const existingAgentSpawn = Array.isArray(existingHooks["agentSpawn"]) ? existingHooks["agentSpawn"].filter((entry) => !isManagedKiroSessionHook(entry)) : [];
13192
+ existingHooks["agentSpawn"] = [
13193
+ ...existingAgentSpawn,
13194
+ { command: KIRO_SESSION_START_COMMAND }
13195
+ ];
13196
+ extras["hooks"] = existingHooks;
13197
+ const json = {
13198
+ name: spec.name,
13199
+ description: spec.description,
13200
+ prompt: spec.instructions
13201
+ };
13202
+ if (spec.model !== void 0) json["model"] = spec.model;
13203
+ if (spec.tools !== void 0 && spec.tools.length > 0) json["tools"] = spec.tools;
13204
+ Object.assign(json, extras);
13205
+ return {
13206
+ ext: agentFileExtensionForTool("kiro"),
13207
+ content: `${JSON.stringify(json, null, 2)}
13208
+ `
13209
+ };
13210
+ }
13054
13211
  function renderForCodex(spec) {
13055
13212
  return {
13056
13213
  ext: agentFileExtensionForTool("codex"),
@@ -13178,6 +13335,43 @@ function reverseFromJoycode(filePath, content) {
13178
13335
  }
13179
13336
  return { ok: true, spec };
13180
13337
  }
13338
+ function reverseFromKiro(filePath, content) {
13339
+ let parsed;
13340
+ try {
13341
+ const raw = JSON.parse(content);
13342
+ if (!isRecord(raw)) return { ok: false, reason: "agent config must be a JSON object" };
13343
+ parsed = raw;
13344
+ } catch (err) {
13345
+ return { ok: false, reason: `parse error: ${err.message}` };
13346
+ }
13347
+ const name = parsed["name"] ?? path35.basename(filePath, ".json");
13348
+ if (!name) return { ok: false, reason: "missing field name" };
13349
+ if (!parsed["description"]) return { ok: false, reason: "missing field description" };
13350
+ if (!parsed["prompt"]) return { ok: false, reason: "missing field prompt" };
13351
+ const extras = {};
13352
+ for (const [key, value] of Object.entries(parsed)) {
13353
+ if (!COMMON_KIRO_FIELDS.has(key)) extras[key] = value;
13354
+ }
13355
+ if (isRecord(extras["hooks"])) {
13356
+ const hooks = { ...extras["hooks"] };
13357
+ if (Array.isArray(hooks["agentSpawn"])) {
13358
+ const remaining = hooks["agentSpawn"].filter((entry) => !isManagedKiroSessionHook(entry));
13359
+ if (remaining.length > 0) hooks["agentSpawn"] = remaining;
13360
+ else delete hooks["agentSpawn"];
13361
+ }
13362
+ if (Object.keys(hooks).length > 0) extras["hooks"] = hooks;
13363
+ else delete extras["hooks"];
13364
+ }
13365
+ const spec = {
13366
+ name,
13367
+ description: parsed["description"],
13368
+ instructions: parsed["prompt"]
13369
+ };
13370
+ if (parsed["model"] !== void 0) spec.model = parsed["model"];
13371
+ if (parsed["tools"] !== void 0) spec.tools = parsed["tools"];
13372
+ if (Object.keys(extras).length > 0) spec.tool_extras = { kiro: extras };
13373
+ return { ok: true, spec };
13374
+ }
13181
13375
  function reverseFromCodex(filePath, content) {
13182
13376
  let parsed;
13183
13377
  try {
@@ -13326,16 +13520,19 @@ function renderForTool(spec, tool) {
13326
13520
  return renderForJoycode(spec);
13327
13521
  case "qoder":
13328
13522
  return renderForClaude(spec);
13523
+ case "kiro":
13524
+ return renderForKiro(spec);
13329
13525
  case "zcode":
13330
13526
  return renderForClaude(spec);
13331
13527
  case "opencode":
13332
13528
  return renderForOpencode(spec);
13333
13529
  }
13334
13530
  }
13335
- var ALL_SUPPORTED_TOOLS, COMMON_CLAUDE_FIELDS, COMMON_CURSOR_FIELDS, COMMON_CODEX_FIELDS, COMMON_OPENCODE_FIELDS, MERGE_COMMON_FIELDS;
13531
+ var ALL_SUPPORTED_TOOLS, KIRO_SESSION_START_COMMAND, COMMON_CLAUDE_FIELDS, COMMON_CURSOR_FIELDS, COMMON_CODEX_FIELDS, COMMON_KIRO_FIELDS, COMMON_OPENCODE_FIELDS, MERGE_COMMON_FIELDS;
13336
13532
  var init_agent_format = __esm({
13337
13533
  "src/resources/agent-format.ts"() {
13338
13534
  "use strict";
13535
+ init_builtin_hooks();
13339
13536
  ALL_SUPPORTED_TOOLS = [
13340
13537
  "claude",
13341
13538
  "claude-internal",
@@ -13347,12 +13544,15 @@ var init_agent_format = __esm({
13347
13544
  "cursor",
13348
13545
  "joycode",
13349
13546
  "qoder",
13547
+ "kiro",
13350
13548
  "zcode",
13351
13549
  "opencode"
13352
13550
  ];
13551
+ KIRO_SESSION_START_COMMAND = getDispatchCommand("session-start", "kiro");
13353
13552
  COMMON_CLAUDE_FIELDS = /* @__PURE__ */ new Set(["name", "description", "model", "tools"]);
13354
13553
  COMMON_CURSOR_FIELDS = /* @__PURE__ */ new Set(["agent_id", "description", "model", "tools"]);
13355
13554
  COMMON_CODEX_FIELDS = /* @__PURE__ */ new Set(["name", "description", "developer_instructions", "model"]);
13555
+ COMMON_KIRO_FIELDS = /* @__PURE__ */ new Set(["name", "description", "prompt", "model", "tools"]);
13356
13556
  COMMON_OPENCODE_FIELDS = /* @__PURE__ */ new Set(["description", "model"]);
13357
13557
  MERGE_COMMON_FIELDS = [
13358
13558
  "name",
@@ -13385,7 +13585,7 @@ async function removeStaleAgentSiblings(targetAgentsDir, stem, targetExt) {
13385
13585
  return;
13386
13586
  }
13387
13587
  for (const file of files) {
13388
- const base = file.replace(/\.(md|toml)$/, "");
13588
+ const base = file.replace(/\.(md|toml|json)$/, "");
13389
13589
  if (base !== stem) continue;
13390
13590
  if (file === `${stem}${targetExt}`) continue;
13391
13591
  try {
@@ -13517,8 +13717,16 @@ function mergeCanonicalEdits(canonical, perTool) {
13517
13717
  function getAgentStem(filename) {
13518
13718
  if (filename.endsWith(".md")) return filename.slice(0, -3);
13519
13719
  if (filename.endsWith(".toml")) return filename.slice(0, -5);
13720
+ if (filename.endsWith(".json")) return filename.slice(0, -5);
13520
13721
  return null;
13521
13722
  }
13723
+ async function removeStaleAgentSiblings2(agentsDir, stem, targetExt) {
13724
+ for (const file of await listFiles(agentsDir)) {
13725
+ if (getAgentStem(file) !== stem || file === `${stem}${targetExt}`) continue;
13726
+ await remove(path37.join(agentsDir, file));
13727
+ log.debug(`Removed stale agent sibling ${file} for ${stem}`);
13728
+ }
13729
+ }
13522
13730
  function isKnownTool(tool) {
13523
13731
  return ALL_SUPPORTED_TOOLS.includes(tool);
13524
13732
  }
@@ -13551,6 +13759,8 @@ function reverseByTool(tool, filePath, content) {
13551
13759
  return reverseFromJoycode(filePath, content);
13552
13760
  case "qoder":
13553
13761
  return reverseFromClaude(filePath, content);
13762
+ case "kiro":
13763
+ return reverseFromKiro(filePath, content);
13554
13764
  case "zcode":
13555
13765
  return reverseFromClaude(filePath, content);
13556
13766
  case "opencode":
@@ -13847,6 +14057,7 @@ var init_agents = __esm({
13847
14057
  try {
13848
14058
  await ensureDir(destDir);
13849
14059
  const { ext, content: rendered } = renderForTool(spec, tool);
14060
+ await removeStaleAgentSiblings2(destDir, item.name, ext);
13850
14061
  const dest = path37.join(destDir, `${item.name}${ext}`);
13851
14062
  await writeFile(dest, rendered);
13852
14063
  log.debug(`Rendered agent ${item.name} \u2192 ${tool} (${ext})`);
@@ -13874,7 +14085,7 @@ var init_agents = __esm({
13874
14085
  await this.addTombstone(name, localConfig);
13875
14086
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
13876
14087
  if (!toolPath.agents) continue;
13877
- for (const ext of [".md", ".toml"]) {
14088
+ for (const ext of [".md", ".toml", ".json"]) {
13878
14089
  const filePath = path37.join(baseDir, toolPath.agents, `${name}${ext}`);
13879
14090
  if (await pathExists(filePath)) {
13880
14091
  await remove(filePath);
@@ -14032,7 +14243,7 @@ async function fetchLatestVersion(registry, timeout = VERSION_CHECK_TIMEOUT) {
14032
14243
  const { stdout } = await execFileAsync2(
14033
14244
  npm.cmd,
14034
14245
  [...npm.args, "view", pkgName, "version", `--registry=${resolvedRegistry}`],
14035
- { timeout, encoding: "utf-8" }
14246
+ { timeout, encoding: "utf-8", windowsHide: true }
14036
14247
  );
14037
14248
  const version2 = stdout.trim();
14038
14249
  if (!version2) return null;
@@ -14266,7 +14477,7 @@ async function doUpdate() {
14266
14477
  ...target ? [`--prefix=${target.prefix}`] : [],
14267
14478
  `--registry=${registry}`
14268
14479
  ],
14269
- { timeout: INSTALL_TIMEOUT }
14480
+ { timeout: INSTALL_TIMEOUT, windowsHide: true }
14270
14481
  );
14271
14482
  log.success(`Updated teamai to v${result.latest}`);
14272
14483
  const entry = resolveTeamaiEntryScript();
@@ -14287,7 +14498,8 @@ async function doUpdate() {
14287
14498
  try {
14288
14499
  const refresh = entry ? { cmd: process.execPath, args: [entry, "hooks", "inject", "--silent"] } : { cmd: "teamai", args: ["hooks", "inject", "--silent"] };
14289
14500
  await execFileAsync2(refresh.cmd, refresh.args, {
14290
- timeout: 15e3
14501
+ timeout: 15e3,
14502
+ windowsHide: true
14291
14503
  });
14292
14504
  log.success("Refreshed hooks with new version");
14293
14505
  } catch (e) {
@@ -14868,8 +15080,17 @@ import fse8 from "fs-extra";
14868
15080
  function businessRoot(localConfig) {
14869
15081
  return localConfig.repo.businessRepoRoot ?? path42.dirname(localConfig.repo.localPath);
14870
15082
  }
15083
+ function reportsGitRoot(localConfig) {
15084
+ if (isSelfMode(localConfig)) {
15085
+ return businessRoot(localConfig);
15086
+ }
15087
+ return localConfig.repo.localPath;
15088
+ }
14871
15089
  function reportsWorktreePath(localConfig) {
14872
- return path42.join(localConfig.repo.localPath, REPORTS_WORKTREE_DIRNAME);
15090
+ return getReportsDir(localConfig);
15091
+ }
15092
+ function reportsLockPath(localConfig) {
15093
+ return path42.join(path42.dirname(getReportsDir(localConfig)), REPORTS_LOCK_FILENAME);
14873
15094
  }
14874
15095
  async function remoteBranchExists2(repoRoot) {
14875
15096
  const git = createGit2(repoRoot);
@@ -14881,10 +15102,22 @@ async function remoteBranchExists2(repoRoot) {
14881
15102
  }
14882
15103
  }
14883
15104
  async function ensureReportsWorktree(localConfig, options = {}) {
15105
+ if (!usesReportsBranch(localConfig)) {
15106
+ return getReportsDir(localConfig);
15107
+ }
14884
15108
  const wt = reportsWorktreePath(localConfig);
14885
- const repoRoot = businessRoot(localConfig);
15109
+ const repoRoot = reportsGitRoot(localConfig);
15110
+ if (!isSelfMode(localConfig) && !await isDedicatedRepoRoot(repoRoot)) {
15111
+ throw new Error(
15112
+ `Refusing to create a reports worktree: ${repoRoot} is not a dedicated team-repo clone root`
15113
+ );
15114
+ }
14886
15115
  if (await isGitRepo(wt)) {
14887
- return wt;
15116
+ try {
15117
+ await createGit2(wt).revparse(["--is-inside-work-tree"]);
15118
+ return wt;
15119
+ } catch {
15120
+ }
14888
15121
  }
14889
15122
  if (await pathExists(wt)) {
14890
15123
  await fse8.remove(wt);
@@ -14907,14 +15140,19 @@ async function ensureReportsWorktree(localConfig, options = {}) {
14907
15140
  await git.raw(["worktree", "add", wt, "--track", "-b", REPORTS_BRANCH, `origin/${REPORTS_BRANCH}`]);
14908
15141
  }
14909
15142
  } 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");
15143
+ const branches = await git.branchLocal();
15144
+ if (branches.all.includes(REPORTS_BRANCH)) {
15145
+ await git.raw(["worktree", "add", wt, REPORTS_BRANCH]);
15146
+ } else {
15147
+ await createOrphanWorktree(repoRoot, wt);
15148
+ await writeWorktreeGitignore(wt);
15149
+ const wtGit = createGit2(wt);
15150
+ await wtGit.add([".gitignore"]);
15151
+ await commitSkippingHooks(wtGit, "[teamai] Initialize reports branch");
15152
+ }
14915
15153
  if (options.pushIfCreated !== false) {
14916
15154
  try {
14917
- await wtGit.push(["-u", "origin", REPORTS_BRANCH]);
15155
+ await createGit2(wt).push(["-u", "origin", REPORTS_BRANCH]);
14918
15156
  } catch (e) {
14919
15157
  log.debug(`[reports] initial push skipped: ${e.message}`);
14920
15158
  }
@@ -14958,8 +15196,8 @@ async function writeWorktreeGitignore(wt) {
14958
15196
  ].join("\n");
14959
15197
  await writeFile(path42.join(wt, ".gitignore"), content);
14960
15198
  }
14961
- async function commitAndPushReports(localConfig, message, files) {
14962
- const lockPath = path42.join(localConfig.repo.localPath, REPORTS_LOCK_FILENAME);
15199
+ async function commitAndPushReports(localConfig, message, files, options = {}) {
15200
+ const lockPath = reportsLockPath(localConfig);
14963
15201
  const locked = await acquireLock(lockPath);
14964
15202
  if (!locked) {
14965
15203
  log.debug("[reports] another reports write is in progress; skipping");
@@ -14970,11 +15208,11 @@ async function commitAndPushReports(localConfig, message, files) {
14970
15208
  const git = createGit2(wt);
14971
15209
  await git.add(files);
14972
15210
  const status2 = await git.status();
14973
- if (status2.staged.length === 0) {
15211
+ if (status2.staged.length === 0 && !options.pushIfUnchanged) {
14974
15212
  log.debug("[reports] nothing to commit");
14975
15213
  return false;
14976
15214
  }
14977
- await commitSkippingHooks(git, message);
15215
+ if (status2.staged.length > 0) await commitSkippingHooks(git, message);
14978
15216
  for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
14979
15217
  try {
14980
15218
  await git.push(["origin", REPORTS_BRANCH]);
@@ -15004,18 +15242,139 @@ async function commitAndPushReports(localConfig, message, files) {
15004
15242
  await releaseLock(lockPath);
15005
15243
  }
15006
15244
  }
15245
+ async function gitPathExists(git, gitPath) {
15246
+ try {
15247
+ const resolved = (await git.raw(["rev-parse", "--git-path", gitPath])).trim();
15248
+ return resolved.length > 0 && await pathExists(resolved);
15249
+ } catch {
15250
+ return false;
15251
+ }
15252
+ }
15253
+ async function rebaseInProgress(git) {
15254
+ return await gitPathExists(git, "rebase-merge") || await gitPathExists(git, "rebase-apply");
15255
+ }
15256
+ async function restoreConflictedFiles(git) {
15257
+ let conflicted = [];
15258
+ try {
15259
+ conflicted = (await git.status()).conflicted ?? [];
15260
+ } catch {
15261
+ return;
15262
+ }
15263
+ if (conflicted.length === 0) {
15264
+ return;
15265
+ }
15266
+ if (await rebaseInProgress(git)) {
15267
+ return;
15268
+ }
15269
+ try {
15270
+ await git.raw(["checkout", "--theirs", "--", ...conflicted]);
15271
+ await git.raw(["add", "--", ...conflicted]);
15272
+ await git.raw(["reset", "HEAD", "--", ...conflicted]);
15273
+ log.debug("[reports] restored uncommitted report files after a stash-apply conflict; using the local copy");
15274
+ } catch (e) {
15275
+ log.debug(`[reports] could not restore stash-apply conflicts: ${e.message}`);
15276
+ try {
15277
+ await git.raw(["reset", "--hard", "HEAD"]);
15278
+ } catch {
15279
+ }
15280
+ }
15281
+ }
15282
+ async function snapshotDirtyTree(git) {
15283
+ try {
15284
+ const sha = (await git.raw(["stash", "create"])).trim();
15285
+ return sha.length > 0 ? sha : null;
15286
+ } catch {
15287
+ return null;
15288
+ }
15289
+ }
15290
+ async function applyDirtySnapshot(git, sha) {
15291
+ try {
15292
+ await git.raw(["stash", "apply", sha]);
15293
+ } catch {
15294
+ }
15295
+ await restoreConflictedFiles(git);
15296
+ }
15297
+ async function syncReportsWorktree(wt) {
15298
+ const git = createGit2(wt);
15299
+ const upstream = `origin/${REPORTS_BRANCH}`;
15300
+ try {
15301
+ await git.fetch(["origin", REPORTS_BRANCH]);
15302
+ } catch (e) {
15303
+ log.debug(`[reports] fetch failed, using the local copy: ${e.message}`);
15304
+ return;
15305
+ }
15306
+ await restoreConflictedFiles(git);
15307
+ const dirty = !(await git.status()).isClean();
15308
+ const ahead = Number.parseInt((await git.raw(["rev-list", "--count", `${upstream}..HEAD`])).trim(), 10);
15309
+ let carried = null;
15310
+ if (dirty && ahead > 0) {
15311
+ carried = await snapshotDirtyTree(git);
15312
+ if (!carried) {
15313
+ log.debug("[reports] uncommitted report files block the refresh; using the local copy");
15314
+ return;
15315
+ }
15316
+ try {
15317
+ await git.raw(["reset", "--hard", "HEAD"]);
15318
+ } catch (e) {
15319
+ log.debug(`[reports] could not clear the worktree for rebase; using the local copy: ${e.message}`);
15320
+ await applyDirtySnapshot(git, carried);
15321
+ return;
15322
+ }
15323
+ }
15324
+ try {
15325
+ if (ahead > 0) {
15326
+ await git.rebase([upstream]);
15327
+ } else {
15328
+ await git.raw(["merge", "--ff-only", upstream]);
15329
+ }
15330
+ } catch (e) {
15331
+ if (ahead > 0) {
15332
+ try {
15333
+ await git.rebase(["--abort"]);
15334
+ } catch {
15335
+ }
15336
+ }
15337
+ if (carried) {
15338
+ await applyDirtySnapshot(git, carried);
15339
+ log.debug(`[reports] uncommitted report files block the refresh; using the local copy: ${e.message}`);
15340
+ return;
15341
+ }
15342
+ if (dirty) {
15343
+ log.debug(`[reports] uncommitted report files block the refresh; using the local copy: ${e.message}`);
15344
+ return;
15345
+ }
15346
+ log.debug(`[reports] dropping ${ahead} unpushed report commit(s) that conflict with ${upstream}: ${e.message}`);
15347
+ await git.raw(["reset", "--hard", upstream]);
15348
+ return;
15349
+ }
15350
+ if (carried) {
15351
+ await applyDirtySnapshot(git, carried);
15352
+ }
15353
+ }
15007
15354
  async function refreshReportsWorktree(localConfig, options = {}) {
15355
+ if (!usesReportsBranch(localConfig)) {
15356
+ return;
15357
+ }
15358
+ const lockPath = reportsLockPath(localConfig);
15359
+ let locked = false;
15008
15360
  try {
15361
+ locked = await acquireLock(lockPath);
15009
15362
  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}`]);
15363
+ if (!locked) {
15364
+ log.debug("[reports] a reports write is in progress; reading the local copy");
15365
+ return;
15366
+ }
15367
+ await syncReportsWorktree(wt);
15013
15368
  } catch (e) {
15014
15369
  log.debug(`[reports] refresh skipped: ${e.message}`);
15370
+ } finally {
15371
+ if (locked) {
15372
+ await releaseLock(lockPath);
15373
+ }
15015
15374
  }
15016
15375
  }
15017
15376
  async function ensureReportsDir(localConfig) {
15018
- if (localConfig.repo.kind === "self") {
15377
+ if (usesReportsBranch(localConfig)) {
15019
15378
  return ensureReportsWorktree(localConfig);
15020
15379
  }
15021
15380
  return getReportsDir(localConfig);
@@ -15139,10 +15498,11 @@ async function listMembers(options) {
15139
15498
  const projectConfig = await detectProjectConfig();
15140
15499
  const localConfig = projectConfig ?? (await requireInit()).localConfig;
15141
15500
  let repoPath;
15142
- if (localConfig.repo.kind === "self") {
15501
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
15502
+ if (usesReportsBranch2(localConfig)) {
15143
15503
  const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
15144
- await refreshReportsWorktree2(localConfig);
15145
- repoPath = await ensureReportsWorktree2(localConfig);
15504
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
15505
+ repoPath = await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
15146
15506
  } else {
15147
15507
  repoPath = localConfig.repo.localPath;
15148
15508
  await pullRepo(repoPath);
@@ -15834,12 +16194,12 @@ async function initSelfRepo(options) {
15834
16194
  return;
15835
16195
  }
15836
16196
  const provider = getProvider(providerName);
15837
- log.debug(`Detected provider: ${providerName} (from ${remoteUrl})`);
16197
+ log.debug(`Detected provider: ${providerName} (from ${redactGitCredentials(remoteUrl)})`);
15838
16198
  let repoInfo;
15839
16199
  try {
15840
- repoInfo = provider.parseRepoInput(remoteUrl);
16200
+ repoInfo = providerName === "git" ? parseGenericGitExistingRemote(remoteUrl) : provider.parseRepoInput(remoteUrl);
15841
16201
  } catch (e) {
15842
- log.error(`Could not parse the business repo remote "${remoteUrl}": ${e.message}`);
16202
+ log.error(`Could not parse the business repo remote "${redactGitCredentials(remoteUrl)}": ${e.message}`);
15843
16203
  process.exit(1);
15844
16204
  return;
15845
16205
  }
@@ -16242,6 +16602,7 @@ async function init(options) {
16242
16602
  const emailDomain = provider.getDefaultEmailDomain() ?? void 0;
16243
16603
  await configureGitUser(localPath, username, username, void 0, emailDomain);
16244
16604
  const teamConfig = await loadTeamConfig(localPath);
16605
+ const createdSkeleton = !teamConfig;
16245
16606
  if (!teamConfig) {
16246
16607
  log.warn("teamai.yaml not found in repo. Creating default config...");
16247
16608
  const defaultConfig = YAML11.stringify({
@@ -16271,33 +16632,59 @@ async function init(options) {
16271
16632
  log.error(error.message);
16272
16633
  process.exit(1);
16273
16634
  }
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, {
16635
+ const reportsConfig = {
16636
+ repo: { localPath, remote: repoInfo.httpsUrl },
16278
16637
  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}`);
16638
+ scope,
16639
+ projectRoot,
16640
+ additionalRoles: []
16641
+ };
16642
+ if (createdSkeleton && !options.dryRun) {
16643
+ try {
16644
+ await pushRepoDirectly(localPath, "[teamai] Initialize team repo skeleton", [
16645
+ "teamai.yaml",
16646
+ "skills/.gitkeep",
16647
+ "rules/.gitkeep",
16648
+ "docs/.gitkeep",
16649
+ "env/.gitkeep",
16650
+ "members/.gitkeep"
16651
+ ]);
16652
+ } catch (e) {
16653
+ log.warn(`Push failed (you can push manually later): ${e.message}`);
16654
+ }
16655
+ }
16656
+ let isNewMember = true;
16657
+ if (!options.dryRun) {
16658
+ try {
16659
+ const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
16660
+ const wt = await ensureReportsWorktree2(reportsConfig);
16661
+ const memberDir = path45.join(wt, "members");
16662
+ await ensureDir(memberDir);
16663
+ const memberPath = path45.join(memberDir, `${username}.yaml`);
16664
+ isNewMember = !await pathExists(memberPath);
16665
+ const existingMember = await getMemberConfig(wt, username);
16666
+ const { config: memberConfig, changed: memberChanged } = mergeMemberConfig(existingMember, {
16667
+ username,
16668
+ projects: resolvedProjects
16669
+ });
16670
+ if (memberChanged) {
16671
+ await writeFile(memberPath, YAML11.stringify(memberConfig));
16672
+ log.success(isNewMember ? `Registered as team member: ${username}` : `Updated member roster: ${username}${memberConfig.projects ? ` (projects: ${memberConfig.projects.join(", ")})` : ""}`);
16673
+ const pushed = await commitAndPushReports2(reportsConfig, isNewMember ? `[teamai] Register member: ${username}` : `[teamai] Update member roster: ${username}`, ["members/"]);
16674
+ if (pushed) {
16675
+ log.success(isNewMember ? "Member registered on the teamai-reports branch" : "Member roster updated on the teamai-reports branch");
16676
+ } else {
16677
+ log.warn("Member registration could not be pushed (no write access?). You are still set up locally.");
16678
+ }
16679
+ } else {
16680
+ log.info(`Member ${username} already registered`);
16681
+ isNewMember = false;
16297
16682
  }
16683
+ } catch (e) {
16684
+ log.warn(`Member registration skipped (non-blocking): ${e.message}`);
16298
16685
  }
16299
16686
  } else {
16300
- log.info(`Member ${username} already registered`);
16687
+ log.info(`[dry-run] Would register member ${username} on the teamai-reports branch`);
16301
16688
  }
16302
16689
  const currentConfig = await loadTeamConfig(localPath);
16303
16690
  const hasReviewers = currentConfig?.reviewers && currentConfig.reviewers.length > 0;
@@ -16425,6 +16812,7 @@ var init_init = __esm({
16425
16812
  init_git2();
16426
16813
  init_git2();
16427
16814
  init_providers();
16815
+ init_repo_url5();
16428
16816
  init_fs();
16429
16817
  init_logger();
16430
16818
  init_types();
@@ -19700,6 +20088,12 @@ function mergeDeltas(local, remote) {
19700
20088
  votes[docId].last_upvoted_at = localEntry.last_upvoted_at;
19701
20089
  }
19702
20090
  }
20091
+ if (votes[docId].recalled_count === 0) {
20092
+ votes[docId].last_recalled_at = "";
20093
+ }
20094
+ if (votes[docId].upvoted_count === 0) {
20095
+ delete votes[docId].last_upvoted_at;
20096
+ }
19703
20097
  }
19704
20098
  return { version: 2, votes, deltas: {} };
19705
20099
  }
@@ -19739,11 +20133,16 @@ async function recallFeedback(opts) {
19739
20133
  return;
19740
20134
  }
19741
20135
  const existingDelta = data.deltas[opts.negative] ?? { recalled_delta: 0, upvoted_delta: 0 };
20136
+ const decrementedCount = entry.upvoted_count - 1;
20137
+ const updatedEntry = { ...entry, upvoted_count: decrementedCount };
20138
+ if (decrementedCount === 0) {
20139
+ delete updatedEntry.last_upvoted_at;
20140
+ }
19742
20141
  const updated = {
19743
20142
  ...data,
19744
20143
  votes: {
19745
20144
  ...data.votes,
19746
- [opts.negative]: { ...entry, upvoted_count: entry.upvoted_count - 1 }
20145
+ [opts.negative]: updatedEntry
19747
20146
  },
19748
20147
  deltas: {
19749
20148
  ...data.deltas,
@@ -21473,10 +21872,11 @@ async function generateDigest(options) {
21473
21872
  const localConfig = projectConfig ?? (await requireInit()).localConfig;
21474
21873
  const repoPath = localConfig.repo.localPath;
21475
21874
  let reportsRoot = repoPath;
21476
- if (localConfig.repo.kind === "self") {
21875
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
21876
+ if (usesReportsBranch2(localConfig)) {
21477
21877
  const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
21478
- await refreshReportsWorktree2(localConfig);
21479
- reportsRoot = await ensureReportsWorktree2(localConfig);
21878
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
21879
+ reportsRoot = await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
21480
21880
  }
21481
21881
  const teamStats = await loadTeamStats(reportsRoot);
21482
21882
  if (teamStats.length === 0) {
@@ -21629,9 +22029,10 @@ async function loadReportedStats() {
21629
22029
  const config = await detectProjectConfig() ?? await loadLocalConfig();
21630
22030
  if (!config) return null;
21631
22031
  let statsRoot = config.repo.localPath;
21632
- if (config.repo.kind === "self") {
22032
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
22033
+ if (usesReportsBranch2(config)) {
21633
22034
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
21634
- statsRoot = await ensureReportsWorktree2(config);
22035
+ statsRoot = await ensureReportsWorktree2(config, { pushIfCreated: false });
21635
22036
  }
21636
22037
  const statsPath = path62.join(statsRoot, "stats", `${config.username}.yaml`);
21637
22038
  const content = await readFileSafe(statsPath);
@@ -22105,8 +22506,9 @@ function filterEventsByScope(events, opts) {
22105
22506
  return events;
22106
22507
  }
22107
22508
  async function reportUsageToTeam(repoPath, username, options) {
22108
- const selfConfig = options?.selfConfig;
22109
- const selfMode = selfConfig?.repo.kind === "self";
22509
+ const reportsConfig = options?.selfConfig;
22510
+ const useReportsBranch = !!reportsConfig && usesReportsBranch(reportsConfig);
22511
+ let restoreStats;
22110
22512
  try {
22111
22513
  const events = await readUsageEvents();
22112
22514
  const filesToPush = [];
@@ -22136,19 +22538,19 @@ async function reportUsageToTeam(repoPath, username, options) {
22136
22538
  const hasPromptTokens = hasPromptTokenDelta(promptTokenDelta);
22137
22539
  const hasDaily = hasDailyDelta(dailyDelta);
22138
22540
  let writeRoot = repoPath;
22139
- if (selfMode && selfConfig) {
22541
+ if (useReportsBranch && reportsConfig) {
22140
22542
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
22141
- writeRoot = await ensureReportsWorktree2(selfConfig);
22543
+ writeRoot = await ensureReportsWorktree2(reportsConfig);
22142
22544
  } else {
22143
22545
  const git = createGit2(repoPath);
22144
22546
  if (!await isDedicatedRepoRoot(repoPath)) {
22145
22547
  log.debug(`Skipping report: ${repoPath} is not a dedicated team-repo root (safety guard)`);
22146
- return;
22548
+ return false;
22147
22549
  }
22148
22550
  const { isImportInProgress: isImportInProgress2 } = await Promise.resolve().then(() => (init_import_lock(), import_lock_exports));
22149
22551
  if (await isImportInProgress2(repoPath)) {
22150
22552
  log.debug(`Skipping report: import in progress for ${repoPath} (would reset uncommitted artifacts)`);
22151
- return;
22553
+ return false;
22152
22554
  }
22153
22555
  const yamlPath = path64.join(repoPath, "teamai.yaml");
22154
22556
  const workingContent = await readFileSafe(yamlPath);
@@ -22168,6 +22570,10 @@ async function reportUsageToTeam(repoPath, username, options) {
22168
22570
  await ensureDir(statsDir);
22169
22571
  const statsPath = path64.join(statsDir, `${username}.yaml`);
22170
22572
  const existing = await readExistingStats(statsPath);
22573
+ if (useReportsBranch) {
22574
+ const previousContent = await readFileSafe(statsPath);
22575
+ restoreStats = () => writeFile(statsPath, previousContent ?? "");
22576
+ }
22171
22577
  const newStats = hasUsage ? aggregateUsage(events) : [];
22172
22578
  const merged = mergeStats(existing, username, newStats);
22173
22579
  if (hasInterventions) {
@@ -22197,23 +22603,20 @@ async function reportUsageToTeam(repoPath, username, options) {
22197
22603
  }
22198
22604
  if (filesToPush.length === 0) {
22199
22605
  log.debug("No usage events or votes to report");
22200
- return;
22606
+ return true;
22201
22607
  }
22202
22608
  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) {
22609
+ if (useReportsBranch && reportsConfig) {
22204
22610
  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
- );
22611
+ if (!await commitAndPushReports2(reportsConfig, commitMsg, filesToPush, { pushIfUnchanged: true })) {
22612
+ log.debug("Auto-report push was not confirmed; keeping local report data");
22613
+ await restoreStats?.();
22614
+ return false;
22615
+ }
22210
22616
  } else {
22211
- await withTimeout(
22212
- pushRepoDirectly(repoPath, commitMsg, filesToPush),
22213
- 5e3,
22214
- "Auto-report timeout (5s)"
22215
- );
22617
+ await pushRepoDirectly(repoPath, commitMsg, filesToPush);
22216
22618
  }
22619
+ restoreStats = void 0;
22217
22620
  if (hasUsage && !options?.skipTruncate) {
22218
22621
  await truncateUsageAfterReport(events.length);
22219
22622
  log.debug(`Reported ${events.length} usage events to team repo`);
@@ -22238,8 +22641,15 @@ async function reportUsageToTeam(repoPath, username, options) {
22238
22641
  if (!hasUsage && !hasInterventions && !hasPromptTokens && !hasDaily) {
22239
22642
  log.debug("Pushed pending votes to team repo");
22240
22643
  }
22644
+ return true;
22241
22645
  } catch (e) {
22646
+ try {
22647
+ await restoreStats?.();
22648
+ } catch (restoreError) {
22649
+ log.error(`Could not restore report stats after failure: ${restoreError.message}`);
22650
+ }
22242
22651
  log.error(`Auto-report skipped: ${e.message}`);
22652
+ return false;
22243
22653
  }
22244
22654
  }
22245
22655
  var init_team_push = __esm({
@@ -22249,7 +22659,6 @@ var init_team_push = __esm({
22249
22659
  init_stats();
22250
22660
  init_dashboard_collector();
22251
22661
  init_git2();
22252
- init_async();
22253
22662
  init_fs();
22254
22663
  init_logger();
22255
22664
  init_types();
@@ -22693,11 +23102,6 @@ async function pullForScope(localConfig, options, policy = {}) {
22693
23102
  const scopeLabel = localConfig.scope;
22694
23103
  const revisionField = policy.revisionField ?? "lastPullRev";
22695
23104
  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
23105
  const pullSpin = spinner(`[${scopeLabel}] Pulling team repo...`).start();
22702
23106
  let currentRev = null;
22703
23107
  let reportingOnly = false;
@@ -22714,39 +23118,39 @@ async function pullForScope(localConfig, options, policy = {}) {
22714
23118
  pullSpin.fail(`[${scopeLabel}] Pull failed: ${e.message}`);
22715
23119
  return;
22716
23120
  }
23121
+ const freshConfig = await loadTeamConfig(localConfig.repo.localPath);
23122
+ if (!freshConfig) {
23123
+ log.warn(`[${scopeLabel}] Team config (teamai.yaml) not found. Skipping.`);
23124
+ return;
23125
+ }
22717
23126
  let currentTargets = null;
22718
23127
  if (!options.force && !options.dryRun && !submodulesChanged) {
22719
23128
  try {
22720
23129
  const state = await loadStateForScope(localConfig);
22721
23130
  if (currentRev && state[revisionField] && state[revisionField] === currentRev) {
22722
- currentTargets = await getInstalledResourceTargets(teamConfig, localConfig);
23131
+ currentTargets = await getInstalledResourceTargets(freshConfig, localConfig);
22723
23132
  const previousTargets = state[targetsField];
22724
23133
  const syncedTargets = new Set(previousTargets ?? []);
22725
23134
  const targetSetMatches = previousTargets !== void 0 && previousTargets.length === currentTargets.length && currentTargets.every((target) => syncedTargets.has(target));
22726
23135
  if (targetSetMatches) {
22727
23136
  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
- }
23137
+ const skipRecall = !isRecallEnabled(localConfig, freshConfig);
23138
+ try {
23139
+ const { deployBuiltinAgents: deployBuiltinAgents2 } = await Promise.resolve().then(() => (init_builtin_agents(), builtin_agents_exports));
23140
+ await deployBuiltinAgents2(freshConfig, localConfig, { skipRecall });
23141
+ } catch {
23142
+ }
23143
+ try {
23144
+ const { deployBuiltinRules: deployBuiltinRules2 } = await Promise.resolve().then(() => (init_builtin_rules(), builtin_rules_exports));
23145
+ await deployBuiltinRules2(freshConfig, localConfig, { skipRecall });
23146
+ } catch {
23147
+ }
23148
+ try {
23149
+ const { deployBuiltinSkills: deployBuiltinSkills2 } = await Promise.resolve().then(() => (init_builtin_skills(), builtin_skills_exports));
23150
+ await deployBuiltinSkills2(freshConfig, localConfig, { reportingOnly, skipRecall });
23151
+ } catch {
22749
23152
  }
23153
+ await injectRecallBlockIntoTools(freshConfig, localConfig, scopeLabel);
22750
23154
  return;
22751
23155
  }
22752
23156
  log.debug(`[${scopeLabel}] Repo unchanged; resource target set changed, syncing`);
@@ -22755,11 +23159,6 @@ async function pullForScope(localConfig, options, policy = {}) {
22755
23159
  log.debug(`[${scopeLabel}] Rev check failed, proceeding with full sync`);
22756
23160
  }
22757
23161
  }
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
23162
  let roleContext = null;
22764
23163
  try {
22765
23164
  roleContext = await buildRolePullContext(localConfig);
@@ -22962,21 +23361,29 @@ async function pullForScope(localConfig, options, policy = {}) {
22962
23361
  if (totalSynced === 0) {
22963
23362
  log.info(`[${scopeLabel}] No resources to sync`);
22964
23363
  }
23364
+ let reportsReadRoot;
23365
+ const resolveReportsReadRoot = () => {
23366
+ reportsReadRoot ??= (async () => {
23367
+ if (!usesReportsBranch(localConfig)) return localConfig.repo.localPath;
23368
+ try {
23369
+ const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
23370
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
23371
+ return await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
23372
+ } catch (e) {
23373
+ log.debug(`reports worktree unavailable: ${e.message}`);
23374
+ return void 0;
23375
+ }
23376
+ })();
23377
+ return reportsReadRoot;
23378
+ };
22965
23379
  if (!options.dryRun) {
22966
23380
  try {
22967
23381
  const learningsRepoDir = path66.join(localConfig.repo.localPath, "learnings");
22968
23382
  const docsRepoDir = path66.join(localConfig.repo.localPath, "docs");
22969
23383
  const rulesRepoDir = path66.join(localConfig.repo.localPath, "rules");
22970
23384
  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
- }
23385
+ const reportsRoot = await resolveReportsReadRoot();
23386
+ const votesDir = reportsRoot ? path66.join(reportsRoot, "votes") : void 0;
22980
23387
  const activeLearningsNamespaces = roleContext?.activeNamespaces.learnings ?? [];
22981
23388
  const countLearnings = async (baseDir) => {
22982
23389
  let n = (await listFiles(baseDir)).filter((f) => f.endsWith(".md")).length;
@@ -23010,7 +23417,7 @@ async function pullForScope(localConfig, options, policy = {}) {
23010
23417
  const repoCodebaseDir = path66.join(localConfig.repo.localPath, "docs", "team-codebase");
23011
23418
  const effectiveCodebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : void 0;
23012
23419
  if (hasAnySource || effectiveCodebaseDir) {
23013
- const votesExist = await pathExists(votesDir);
23420
+ const votesExist = votesDir ? await pathExists(votesDir) : false;
23014
23421
  const teamaiHome = getDataHome(localConfig);
23015
23422
  const indexPath = path66.join(teamaiHome, "search-index.json");
23016
23423
  const { buildIndex: buildIndex2 } = await Promise.resolve().then(() => (init_search_index(), search_index_exports));
@@ -23156,16 +23563,9 @@ async function pullForScope(localConfig, options, policy = {}) {
23156
23563
  const YAML25 = (await import("yaml")).default;
23157
23564
  const { listFiles: listFiles2, readFileSafe: readFileSafe5 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
23158
23565
  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);
23566
+ const reportsRoot = await resolveReportsReadRoot();
23567
+ const statsDir = reportsRoot ? path66.join(reportsRoot, "stats") : void 0;
23568
+ const files = statsDir ? await listFiles2(statsDir) : [];
23169
23569
  const teamStats = [];
23170
23570
  for (const file of files) {
23171
23571
  if (!file.endsWith(".yaml")) continue;
@@ -23394,6 +23794,7 @@ async function pull(options) {
23394
23794
  const needsHookMigration = await legacyHooksNeedReinject().catch(() => false);
23395
23795
  const contended = /* @__PURE__ */ new Set();
23396
23796
  const heldLocks = /* @__PURE__ */ new Map();
23797
+ let usageReport;
23397
23798
  const lockScope = async (config) => {
23398
23799
  if (config.repo.kind === "http") return true;
23399
23800
  const lock = path66.join(getDataHome(config), SYNC_LOCK_FILENAME);
@@ -23469,49 +23870,62 @@ async function pull(options) {
23469
23870
  await reconcileHooksAllScopes(reconcileUser, reconcileProject, options);
23470
23871
  await reconcileMcpAllScopes(reconcileUser, reconcileProject, options);
23471
23872
  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 } : {}
23873
+ if (!options.dryRun && !pendingUsageReport) {
23874
+ pendingUsageReport = (async () => {
23875
+ try {
23876
+ const { reportUsageToTeam: reportUsageToTeam2 } = await Promise.resolve().then(() => (init_team_push(), team_push_exports));
23877
+ const { truncateUsageAfterReport: truncateUsageAfterReport2, readUsageEvents: readUsageEvents2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
23878
+ const targets = [];
23879
+ if (reconcileProject && reconcileProject.repo.kind !== "http" && !await usageReportDisabled(reconcileProject.repo.localPath)) {
23880
+ targets.push({
23881
+ repoPath: reconcileProject.repo.localPath,
23882
+ username: reconcileProject.username,
23883
+ opts: {
23884
+ skipTruncate: true,
23885
+ projectRoot: reconcileProject.projectRoot,
23886
+ // Non-HTTP repos route stats/votes to the teamai-reports orphan branch.
23887
+ selfConfig: reconcileProject
23888
+ }
23889
+ });
23890
+ }
23891
+ if (reconcileUser && reconcileUser.repo.kind !== "http" && !await usageReportDisabled(reconcileUser.repo.localPath)) {
23892
+ targets.push({
23893
+ repoPath: reconcileUser.repo.localPath,
23894
+ username: reconcileUser.username,
23895
+ opts: {
23896
+ skipTruncate: true,
23897
+ excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [],
23898
+ // Non-HTTP repos route stats/votes to the teamai-reports orphan branch
23899
+ // never reset/pull the default branch (or, in self mode, the business tree).
23900
+ selfConfig: reconcileUser
23901
+ }
23902
+ });
23903
+ }
23904
+ const eventCount = (await readUsageEvents2()).length;
23905
+ let allReported = true;
23906
+ for (const t of targets) {
23907
+ try {
23908
+ const reported = await reportUsageToTeam2(t.repoPath, t.username, t.opts);
23909
+ if (!reported) allReported = false;
23910
+ } catch (e) {
23911
+ allReported = false;
23912
+ log.error(`Auto-report to ${t.repoPath} skipped: ${e.message}`);
23499
23913
  }
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
23914
  }
23915
+ if (allReported && eventCount > 0 && targets.length > 0) {
23916
+ await truncateUsageAfterReport2(eventCount);
23917
+ }
23918
+ } catch (e) {
23919
+ log.debug(`Auto-report skipped: ${e.message}`);
23509
23920
  }
23510
- if (eventCount > 0 && targets.length > 0) {
23511
- await truncateUsageAfterReport2(eventCount);
23512
- }
23921
+ })().finally(() => {
23922
+ pendingUsageReport = void 0;
23923
+ });
23924
+ usageReport = pendingUsageReport;
23925
+ try {
23926
+ await withTimeout(pendingUsageReport, 5e3, "Auto-report is still running after 5s");
23513
23927
  } catch (e) {
23514
- log.debug(`Auto-report skipped: ${e.message}`);
23928
+ log.debug(e.message);
23515
23929
  }
23516
23930
  }
23517
23931
  const sourceConfig = reconcileProject ?? reconcileUser;
@@ -23524,8 +23938,15 @@ async function pull(options) {
23524
23938
  }
23525
23939
  }
23526
23940
  } finally {
23527
- for (const lock of heldLocks.values()) {
23528
- await releaseLock(lock);
23941
+ const releaseSyncLocks = async () => {
23942
+ for (const lock of heldLocks.values()) await releaseLock(lock);
23943
+ };
23944
+ if (usageReport && usageReport === pendingUsageReport) {
23945
+ void usageReport.then(releaseSyncLocks, releaseSyncLocks).catch((e) => {
23946
+ log.error(`Could not release report sync locks: ${e.message}`);
23947
+ });
23948
+ } else {
23949
+ await releaseSyncLocks();
23529
23950
  }
23530
23951
  }
23531
23952
  }
@@ -23600,7 +24021,7 @@ async function reconcileCoAuthorAllScopes(userConfig, projectConfig, options) {
23600
24021
  }
23601
24022
  }
23602
24023
  }
23603
- var CONTRIBUTORS_FILE2;
24024
+ var pendingUsageReport, CONTRIBUTORS_FILE2;
23604
24025
  var init_pull = __esm({
23605
24026
  "src/pull.ts"() {
23606
24027
  "use strict";
@@ -23621,6 +24042,7 @@ var init_pull = __esm({
23621
24042
  init_home();
23622
24043
  init_update();
23623
24044
  init_learnings_mirror();
24045
+ init_async();
23624
24046
  CONTRIBUTORS_FILE2 = "CONTRIBUTORS";
23625
24047
  }
23626
24048
  });
@@ -25120,21 +25542,22 @@ async function projectsSet(ids, _options) {
25120
25542
  }
25121
25543
  async function projectsMembers(projectId, _options) {
25122
25544
  const { localConfig } = await autoDetectInit();
25123
- let repoPath;
25124
- if (localConfig.repo.kind === "self") {
25545
+ const knowledgePath = localConfig.repo.localPath;
25546
+ let membersRoot = knowledgePath;
25547
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
25548
+ if (usesReportsBranch2(localConfig)) {
25125
25549
  const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
25126
- await refreshReportsWorktree2(localConfig);
25127
- repoPath = await ensureReportsWorktree2(localConfig);
25550
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
25551
+ membersRoot = await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
25128
25552
  } else {
25129
- repoPath = localConfig.repo.localPath;
25130
- await pullRepo(repoPath).catch(() => {
25553
+ await pullRepo(knowledgePath).catch(() => {
25131
25554
  });
25132
25555
  }
25133
- const manifest = await loadProjectsManifest(repoPath);
25556
+ const manifest = await loadProjectsManifest(knowledgePath);
25134
25557
  if (manifest && !listProjectIds(manifest).includes(projectId)) {
25135
25558
  log.warn(`Project "${projectId}" is not defined in manifest/projects.yaml.`);
25136
25559
  }
25137
- const membersDir = path72.join(repoPath, "members");
25560
+ const membersDir = path72.join(membersRoot, "members");
25138
25561
  const files = (await listFiles(membersDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
25139
25562
  const members = [];
25140
25563
  for (const file of files) {
@@ -25513,8 +25936,8 @@ async function discoverToolResources(tool, toolPath, baseDir, teamSkillNames, te
25513
25936
  const agentsDir = path74.join(baseDir, toolPath.agents);
25514
25937
  if (await pathExists(agentsDir)) {
25515
25938
  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)$/, "");
25939
+ if (!file.endsWith(".md") && !file.endsWith(".toml") && !file.endsWith(".json")) continue;
25940
+ const name = path74.basename(file).replace(/\.(md|toml|json)$/, "");
25518
25941
  if (!teamAgentNames.has(name) && !BUILTIN_AGENT_NAMES.has(name)) continue;
25519
25942
  res.agentFiles.push(path74.join(agentsDir, file));
25520
25943
  }
@@ -26209,7 +26632,8 @@ async function hooksList(_options) {
26209
26632
  for (const d of teamDefs) {
26210
26633
  const matcher = d.matcher ? ` [${d.matcher}]` : "";
26211
26634
  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})`);
26635
+ const roles = d.roles ? `, roles: ${d.roles.length > 0 ? d.roles.join(",") : "nobody"}` : "";
26636
+ console.log(` [${d.key}] ${d.event}${matcher} \u2192 ${d.command} (tools: ${tools}${roles})`);
26213
26637
  }
26214
26638
  }
26215
26639
  console.log("");
@@ -26269,6 +26693,7 @@ async function mcpList(_options) {
26269
26693
  console.log(` ${s.name} [${s.transport}]`);
26270
26694
  if (s.description) console.log(` ${s.description}`);
26271
26695
  console.log(` endpoint: ${endpoint}`);
26696
+ if (s.roles) console.log(` roles: ${s.roles.length > 0 ? s.roles.join(", ") : "nobody"}`);
26272
26697
  const needed = referencedVars(s);
26273
26698
  if (needed.length > 0) {
26274
26699
  const missing = needed.filter((v) => !vars[v]);
@@ -26539,7 +26964,7 @@ async function saveSession(options) {
26539
26964
  log.info(`[dry-run] Would push session summary to sessions/${username}/${monthKey(summary)}.md`);
26540
26965
  return;
26541
26966
  }
26542
- if (localConfig.repo.kind === "self") {
26967
+ if (usesReportsBranch(localConfig)) {
26543
26968
  const spin2 = spinner("Pushing session summary to team...").start();
26544
26969
  try {
26545
26970
  const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
@@ -28670,9 +29095,10 @@ async function resolveVizRoot(opts) {
28670
29095
  }
28671
29096
  const config = await detectProjectConfig() ?? await loadLocalConfig();
28672
29097
  if (config?.repo?.localPath) {
28673
- if (config.repo.kind === "self") {
29098
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
29099
+ if (usesReportsBranch2(config)) {
28674
29100
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
28675
- await ensureReportsWorktree2(config);
29101
+ await ensureReportsWorktree2(config, { pushIfCreated: false });
28676
29102
  }
28677
29103
  const knowledgeRoot = getKnowledgeDir(config);
28678
29104
  const reportsRoot = getReportsDir(config);
@@ -30439,6 +30865,17 @@ var init_mr_hint = __esm({
30439
30865
 
30440
30866
  // src/hook-handlers.ts
30441
30867
  import path92 from "path";
30868
+ async function teamCorrectionKeywords(stdin) {
30869
+ if (typeof stdin.prompt !== "string") return [];
30870
+ try {
30871
+ const { autoDetectInit: autoDetectInit2 } = await Promise.resolve().then(() => (init_config(), config_exports));
30872
+ const { getInterventionSharing: getInterventionSharing2 } = await Promise.resolve().then(() => (init_types(), types_exports));
30873
+ const { teamConfig } = await autoDetectInit2();
30874
+ return getInterventionSharing2(teamConfig).correctionKeywords;
30875
+ } catch {
30876
+ return [];
30877
+ }
30878
+ }
30442
30879
  async function contributeHintAllowed() {
30443
30880
  const { isContributeHintEnabled: isContributeHintEnabled2 } = await Promise.resolve().then(() => (init_types(), types_exports));
30444
30881
  try {
@@ -30539,7 +30976,7 @@ var init_hook_handlers = __esm({
30539
30976
  async execute(stdin, tool) {
30540
30977
  const { parseHookEvent: parseHookEvent2, appendEvent: appendEvent2, compactEvents: compactEvents2 } = await Promise.resolve().then(() => (init_dashboard_collector(), dashboard_collector_exports));
30541
30978
  const raw = JSON.stringify(stdin);
30542
- const event = await parseHookEvent2(raw, tool);
30979
+ const event = await parseHookEvent2(raw, tool, { correctionKeywords: await teamCorrectionKeywords(stdin) });
30543
30980
  if (event) {
30544
30981
  await appendEvent2(event);
30545
30982
  compactEvents2().catch(() => {
@@ -30664,7 +31101,8 @@ var init_hook_handlers = __esm({
30664
31101
  if (verifiedDocIds.length > 0) {
30665
31102
  await incrementUpvoted2(votePath, verifiedDocIds);
30666
31103
  }
30667
- if (localConfig.repo.kind === "self") {
31104
+ const { usesReportsBranch: usesReportsBranch2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31105
+ if (usesReportsBranch2(localConfig)) {
30668
31106
  try {
30669
31107
  const { ensureReportsWorktree: ensureReportsWorktree2, commitAndPushReports: commitAndPushReports2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
30670
31108
  const wt = await ensureReportsWorktree2(localConfig);
@@ -30767,7 +31205,8 @@ var init_hook_handlers = __esm({
30767
31205
  // src/hook-dispatch-cli.ts
30768
31206
  var hook_dispatch_cli_exports = {};
30769
31207
  __export(hook_dispatch_cli_exports, {
30770
- hookDispatchCli: () => hookDispatchCli
31208
+ hookDispatchCli: () => hookDispatchCli,
31209
+ parseStdin: () => parseStdin
30771
31210
  });
30772
31211
  import { spawn as spawn3 } from "child_process";
30773
31212
  async function readStdin4() {
@@ -30828,10 +31267,15 @@ function parseStdin(raw, event) {
30828
31267
  try {
30829
31268
  stdin = JSON.parse(raw);
30830
31269
  } catch {
30831
- log.debug(`hook-dispatch: failed to parse STDIN JSON for event=${event}`);
30832
- return null;
31270
+ const preview = raw.length > 160 ? `${raw.slice(0, 80)}...${raw.slice(-80)}` : raw;
31271
+ log.debug(
31272
+ `hook-dispatch: failed to parse STDIN JSON for event=${event} (len=${raw.length}, body=${JSON.stringify(preview)})`
31273
+ );
30833
31274
  }
30834
31275
  }
31276
+ if (!stdin || typeof stdin !== "object" || Array.isArray(stdin)) {
31277
+ stdin = {};
31278
+ }
30835
31279
  if (!stdin.hook_event_name) {
30836
31280
  const EVENT_MAP2 = {
30837
31281
  "session-start": "SessionStart",
@@ -30857,7 +31301,6 @@ async function hookDispatchCli(event, tool, matcher, bgOnly = false) {
30857
31301
  try {
30858
31302
  const raw = await readStdin4();
30859
31303
  const stdin = parseStdin(raw, event);
30860
- if (stdin === null) return;
30861
31304
  const { loadLocalConfig: loadLocalConfig4, detectProjectConfig: detectProjectConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
30862
31305
  const cwd = resolveHookCwd(stdin);
30863
31306
  if (cwd) {
@@ -30925,7 +31368,8 @@ async function rebuildIndexAfterContribute(localConfig) {
30925
31368
  const docsRepoDir = path93.join(repoPath, "docs");
30926
31369
  const rulesRepoDir = path93.join(repoPath, "rules");
30927
31370
  const skillsRepoDir = path93.join(repoPath, "skills");
30928
- const votesDir = path93.join(repoPath, "votes");
31371
+ const { getReportsDir: getReportsDir2 } = await Promise.resolve().then(() => (init_types(), types_exports));
31372
+ const votesDir = path93.join(getReportsDir2(localConfig), "votes");
30929
31373
  let effectiveLearningsDir;
30930
31374
  const activeLearningsNamespaces = await resolveActiveLearningsNamespaces(
30931
31375
  repoPath,
@@ -31075,7 +31519,7 @@ async function contributeSelf(localConfig, content, options) {
31075
31519
  let votesDir;
31076
31520
  try {
31077
31521
  const { ensureReportsWorktree: ensureReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
31078
- const candidate = path93.join(await ensureReportsWorktree2(localConfig), "votes");
31522
+ const candidate = path93.join(await ensureReportsWorktree2(localConfig, { pushIfCreated: false }), "votes");
31079
31523
  if (await pathExists3(candidate)) votesDir = candidate;
31080
31524
  } catch {
31081
31525
  }
@@ -42412,7 +42856,7 @@ var init_extract_mr = __esm({
42412
42856
  // src/maintenance/paths.ts
42413
42857
  import path130 from "path";
42414
42858
  async function resolveMaintenancePaths(localConfig) {
42415
- if (isSelfMode(localConfig)) {
42859
+ if (usesReportsBranch(localConfig)) {
42416
42860
  const { refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
42417
42861
  await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
42418
42862
  }