@hua-labs/tap 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/commands/init.ts
2
2
  import * as fs6 from "fs";
3
3
  import * as path6 from "path";
4
+ import { execSync } from "child_process";
4
5
 
5
6
  // src/state.ts
6
7
  import * as fs3 from "fs";
@@ -186,7 +187,7 @@ function loadJsonFile(filePath) {
186
187
  return null;
187
188
  }
188
189
  }
189
- function loadSharedConfig(repoRoot) {
190
+ function loadSharedConfig2(repoRoot) {
190
191
  return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
191
192
  }
192
193
  function loadLocalConfig(repoRoot) {
@@ -210,7 +211,7 @@ function loadLegacyShellConfig(repoRoot) {
210
211
  }
211
212
  function resolveConfig(overrides = {}, startDir) {
212
213
  const repoRoot = findRepoRoot2(startDir);
213
- const shared = loadSharedConfig(repoRoot) ?? {};
214
+ const shared = loadSharedConfig2(repoRoot) ?? {};
214
215
  const local = loadLocalConfig(repoRoot) ?? {};
215
216
  const legacy = loadLegacyShellConfig(repoRoot) ?? {};
216
217
  const sources = {
@@ -218,7 +219,8 @@ function resolveConfig(overrides = {}, startDir) {
218
219
  commsDir: "auto",
219
220
  stateDir: "auto",
220
221
  runtimeCommand: "auto",
221
- appServerUrl: "auto"
222
+ appServerUrl: "auto",
223
+ towerName: "auto"
222
224
  };
223
225
  let commsDir;
224
226
  if (overrides.commsDir) {
@@ -287,11 +289,25 @@ function resolveConfig(overrides = {}, startDir) {
287
289
  } else {
288
290
  appServerUrl = DEFAULT_APP_SERVER_URL;
289
291
  }
292
+ const towerName = local.towerName ?? shared.towerName ?? null;
290
293
  return {
291
- config: { repoRoot, commsDir, stateDir, runtimeCommand, appServerUrl },
294
+ config: {
295
+ repoRoot,
296
+ commsDir,
297
+ stateDir,
298
+ runtimeCommand,
299
+ appServerUrl,
300
+ towerName
301
+ },
292
302
  sources
293
303
  };
294
304
  }
305
+ function saveSharedConfig(repoRoot, config) {
306
+ const filePath = path2.join(repoRoot, SHARED_CONFIG_FILE);
307
+ const tmp = `${filePath}.tmp.${process.pid}`;
308
+ fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
309
+ fs2.renameSync(tmp, filePath);
310
+ }
295
311
  function resolvePath(repoRoot, p) {
296
312
  const normalized = normalizeTapPath(p);
297
313
  return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
@@ -753,6 +769,64 @@ async function initCommand(args) {
753
769
  };
754
770
  }
755
771
  logHeader("@hua-labs/tap init");
772
+ const commsRepoIdx = args.indexOf("--comms-repo");
773
+ const commsRepoUrl = commsRepoIdx !== -1 && args[commsRepoIdx + 1] ? args[commsRepoIdx + 1] : void 0;
774
+ if (commsRepoUrl) {
775
+ if (fs6.existsSync(commsDir) && fs6.readdirSync(commsDir).length > 0) {
776
+ const gitDir = path6.join(commsDir, ".git");
777
+ if (fs6.existsSync(gitDir)) {
778
+ log(`Comms directory exists: ${commsDir}`);
779
+ logSuccess("Comms directory is already a git repo \u2014 linking only");
780
+ } else {
781
+ logError(`Comms directory exists but is not a git repo: ${commsDir}`);
782
+ return {
783
+ ok: false,
784
+ command: "init",
785
+ code: "TAP_INIT_CLONE_FAILED",
786
+ message: `Comms directory "${commsDir}" exists but is not a git repo. Remove it or use --force to reinitialize.`,
787
+ warnings: [],
788
+ data: { commsDir, commsRepoUrl }
789
+ };
790
+ }
791
+ } else {
792
+ log(`Cloning comms repo: ${commsRepoUrl}`);
793
+ try {
794
+ execSync(`git clone "${commsRepoUrl}" "${commsDir}"`, {
795
+ stdio: "pipe",
796
+ encoding: "utf-8"
797
+ });
798
+ logSuccess(`Cloned comms repo to ${commsDir}`);
799
+ } catch (err) {
800
+ const msg = err instanceof Error ? err.message : String(err);
801
+ logError(`Failed to clone comms repo: ${msg}`);
802
+ return {
803
+ ok: false,
804
+ command: "init",
805
+ code: "TAP_INIT_CLONE_FAILED",
806
+ message: `Failed to clone comms repo: ${msg}`,
807
+ warnings: [],
808
+ data: { commsRepoUrl }
809
+ };
810
+ }
811
+ }
812
+ }
813
+ {
814
+ const sharedConfig = loadSharedConfig2(repoRoot) ?? {};
815
+ let configChanged = false;
816
+ if (commsRepoUrl) {
817
+ sharedConfig.commsRepoUrl = commsRepoUrl;
818
+ configChanged = true;
819
+ }
820
+ const commsDirRelative = path6.relative(repoRoot, commsDir);
821
+ if (commsDirRelative && commsDirRelative !== "tap-comms") {
822
+ sharedConfig.commsDir = commsDirRelative;
823
+ configChanged = true;
824
+ }
825
+ if (configChanged) {
826
+ saveSharedConfig(repoRoot, sharedConfig);
827
+ logSuccess("Saved comms config to tap-config.json");
828
+ }
829
+ }
756
830
  log(`Comms directory: ${commsDir}`);
757
831
  for (const dir of COMMS_DIRS) {
758
832
  const dirPath = path6.join(commsDir, dir);
@@ -829,7 +903,7 @@ ${entry}
829
903
  // src/adapters/claude.ts
830
904
  import * as fs8 from "fs";
831
905
  import * as path8 from "path";
832
- import { execSync } from "child_process";
906
+ import { execSync as execSync2 } from "child_process";
833
907
 
834
908
  // src/adapters/common.ts
835
909
  import * as fs7 from "fs";
@@ -870,6 +944,10 @@ function canWriteOrCreate(filePath) {
870
944
  return false;
871
945
  }
872
946
  }
947
+ function isEphemeralPath(p) {
948
+ const normalized = p.replace(/\\/g, "/").toLowerCase();
949
+ return normalized.includes("/_npx/") || normalized.includes("\\_npx\\") || normalized.includes("/fnm_multishells/") || normalized.includes("\\fnm_multishells\\") || normalized.includes("/tmp/") || normalized.includes("\\temp\\");
950
+ }
873
951
  function findLocalTapCommsSource(ctx) {
874
952
  const candidates = [
875
953
  path7.join(
@@ -929,8 +1007,10 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
929
1007
  const warnings = [];
930
1008
  const issues = [];
931
1009
  const env = {
932
- TAP_AGENT_NAME: "<set-per-session>",
933
- TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
1010
+ TAP_AGENT_NAME: ctx.agentName ?? "<set-per-session>",
1011
+ TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir),
1012
+ TAP_STATE_DIR: toForwardSlashPath(ctx.stateDir),
1013
+ TAP_REPO_ROOT: toForwardSlashPath(ctx.repoRoot)
934
1014
  };
935
1015
  if (instanceId) {
936
1016
  env.TAP_AGENT_ID = instanceId;
@@ -943,11 +1023,29 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
943
1023
  }
944
1024
  const isBundled = sourcePath.endsWith(".mjs");
945
1025
  let command = bunCommand;
1026
+ let args = [toForwardSlashPath(sourcePath)];
946
1027
  if (!command && isBundled) {
947
- command = process.execPath;
948
- warnings.push(
949
- "bun not found; using node to run the compiled MCP server. Install bun for better performance."
950
- );
1028
+ const isEphemeralSource = isEphemeralPath(sourcePath);
1029
+ const isEphemeralNode = isEphemeralPath(process.execPath);
1030
+ if (isEphemeralSource) {
1031
+ command = "npx";
1032
+ args = ["@hua-labs/tap", "serve"];
1033
+ warnings.push(
1034
+ "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
1035
+ );
1036
+ } else if (isEphemeralNode) {
1037
+ command = "node";
1038
+ warnings.push(
1039
+ "Detected ephemeral node path. Using `node` from PATH for MCP config stability."
1040
+ );
1041
+ } else {
1042
+ command = toForwardSlashPath(process.execPath);
1043
+ }
1044
+ if (!isEphemeralSource) {
1045
+ warnings.push(
1046
+ "bun not found; using node to run the compiled MCP server. Install bun for better performance."
1047
+ );
1048
+ }
951
1049
  }
952
1050
  if (!command) {
953
1051
  issues.push(
@@ -956,8 +1054,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
956
1054
  return { command: null, args: [], env, sourcePath, warnings, issues };
957
1055
  }
958
1056
  return {
959
- command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
960
- args: [toForwardSlashPath(sourcePath)],
1057
+ command,
1058
+ args,
961
1059
  env,
962
1060
  sourcePath,
963
1061
  warnings,
@@ -972,7 +1070,7 @@ function findMcpJsonPath(ctx) {
972
1070
  }
973
1071
  function findClaudeCommand() {
974
1072
  try {
975
- execSync("claude --version", { stdio: "pipe" });
1073
+ execSync2("claude --version", { stdio: "pipe" });
976
1074
  return "claude";
977
1075
  } catch {
978
1076
  return null;
@@ -1834,13 +1932,13 @@ import * as fs13 from "fs";
1834
1932
  import * as net from "net";
1835
1933
  import * as path13 from "path";
1836
1934
  import { randomBytes } from "crypto";
1837
- import { spawn, spawnSync as spawnSync2, execSync as execSync3 } from "child_process";
1935
+ import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
1838
1936
  import { fileURLToPath as fileURLToPath4 } from "url";
1839
1937
 
1840
1938
  // src/runtime/resolve-node.ts
1841
1939
  import * as fs12 from "fs";
1842
1940
  import * as path12 from "path";
1843
- import { execSync as execSync2 } from "child_process";
1941
+ import { execSync as execSync3 } from "child_process";
1844
1942
  function readNodeVersion(repoRoot) {
1845
1943
  const nvFile = path12.join(repoRoot, ".node-version");
1846
1944
  if (!fs12.existsSync(nvFile)) return null;
@@ -1883,7 +1981,7 @@ function probeFnmNode(desiredVersion) {
1883
1981
  );
1884
1982
  if (!fs12.existsSync(candidate)) continue;
1885
1983
  try {
1886
- const v = execSync2(`"${candidate}" --version`, {
1984
+ const v = execSync3(`"${candidate}" --version`, {
1887
1985
  encoding: "utf-8",
1888
1986
  timeout: 5e3
1889
1987
  }).trim();
@@ -1897,7 +1995,7 @@ function probeFnmNode(desiredVersion) {
1897
1995
  }
1898
1996
  function detectNodeMajorVersion(command) {
1899
1997
  try {
1900
- const version2 = execSync2(`"${command}" --version`, {
1998
+ const version2 = execSync3(`"${command}" --version`, {
1901
1999
  encoding: "utf-8",
1902
2000
  timeout: 5e3
1903
2001
  }).trim();
@@ -1911,7 +2009,7 @@ function checkStripTypesSupport(command) {
1911
2009
  const major = detectNodeMajorVersion(command);
1912
2010
  if (major !== null && major >= 22) return true;
1913
2011
  try {
1914
- execSync2(`"${command}" --experimental-strip-types -e ""`, {
2012
+ execSync3(`"${command}" --experimental-strip-types -e ""`, {
1915
2013
  timeout: 5e3,
1916
2014
  stdio: "pipe"
1917
2015
  });
@@ -2469,7 +2567,7 @@ async function terminateProcess(pid, platform) {
2469
2567
  }
2470
2568
  try {
2471
2569
  if (platform === "win32") {
2472
- execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2570
+ execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2473
2571
  } else {
2474
2572
  process.kill(pid, "SIGTERM");
2475
2573
  await delay(2e3);
@@ -2814,6 +2912,40 @@ function isBridgeRunning(stateDir, instanceId) {
2814
2912
  if (!state) return false;
2815
2913
  return isProcessAlive(state.pid);
2816
2914
  }
2915
+ function resolveAgentName(instanceId, explicit, context) {
2916
+ if (explicit) return explicit;
2917
+ try {
2918
+ const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
2919
+ const state = loadState(repoRoot);
2920
+ const stateAgent = state?.instances[instanceId]?.agentName;
2921
+ if (stateAgent) return stateAgent;
2922
+ } catch {
2923
+ }
2924
+ return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
2925
+ }
2926
+ function inferRestartMode(bridgeState, flags, savedMode) {
2927
+ const wasManaged = bridgeState?.appServer != null;
2928
+ const hadAuth = bridgeState?.appServer?.auth != null;
2929
+ const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
2930
+ const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
2931
+ return { manageAppServer, noAuth };
2932
+ }
2933
+ function cleanupHeadlessDispatch(inboxDir, agentName) {
2934
+ const removed = [];
2935
+ if (!fs13.existsSync(inboxDir)) return removed;
2936
+ const normalizedAgent = agentName.replace(/-/g, "_");
2937
+ const marker = `-headless-${normalizedAgent}-review-`;
2938
+ try {
2939
+ for (const file of fs13.readdirSync(inboxDir)) {
2940
+ if (file.includes(marker)) {
2941
+ fs13.unlinkSync(path13.join(inboxDir, file));
2942
+ removed.push(file);
2943
+ }
2944
+ }
2945
+ } catch {
2946
+ }
2947
+ return removed;
2948
+ }
2817
2949
  async function startBridge(options) {
2818
2950
  const {
2819
2951
  instanceId,
@@ -2824,7 +2956,10 @@ async function startBridge(options) {
2824
2956
  agentName,
2825
2957
  port
2826
2958
  } = options;
2827
- const resolvedAgent = agentName || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
2959
+ const resolvedAgent = resolveAgentName(instanceId, agentName, {
2960
+ repoRoot: options.repoRoot,
2961
+ stateDir
2962
+ });
2828
2963
  if (!resolvedAgent) {
2829
2964
  throw new Error(
2830
2965
  `No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
@@ -2976,6 +3111,35 @@ async function stopBridge(options) {
2976
3111
  clearBridgeState(stateDir, instanceId);
2977
3112
  return true;
2978
3113
  }
3114
+ async function restartBridge(options) {
3115
+ const { instanceId, stateDir, platform } = options;
3116
+ const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
3117
+ const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
3118
+ const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
3119
+ const heartbeatPath = path13.join(runtimeStateDir, "heartbeat.json");
3120
+ if (fs13.existsSync(heartbeatPath)) {
3121
+ const startWait = Date.now();
3122
+ while (Date.now() - startWait < drainTimeout) {
3123
+ try {
3124
+ const hb = JSON.parse(fs13.readFileSync(heartbeatPath, "utf-8"));
3125
+ if (!hb.activeTurnId) break;
3126
+ } catch {
3127
+ break;
3128
+ }
3129
+ await new Promise((r) => setTimeout(r, 1e3));
3130
+ }
3131
+ }
3132
+ if (options.headless?.enabled && options.commsDir) {
3133
+ const agentName = options.agentName ?? instanceId;
3134
+ cleanupHeadlessDispatch(path13.join(options.commsDir, "inbox"), agentName);
3135
+ }
3136
+ await stopBridge({ instanceId, stateDir, platform });
3137
+ const restartOptions = {
3138
+ ...options,
3139
+ processExistingMessages: true
3140
+ };
3141
+ return startBridge(restartOptions);
3142
+ }
2979
3143
  function rotateLog(logPath) {
2980
3144
  if (!fs13.existsSync(logPath)) return;
2981
3145
  try {
@@ -3127,7 +3291,13 @@ async function addCommand(args) {
3127
3291
  logHeader(`@hua-labs/tap add ${instanceId}`);
3128
3292
  if (instanceName) log(`Instance name: ${instanceName}`);
3129
3293
  if (port !== null) log(`Port: ${port}`);
3130
- const ctx = { ...createAdapterContext(state.commsDir, repoRoot), instanceId };
3294
+ const existingAgentName = state.instances[instanceId]?.agentName ?? null;
3295
+ const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
3296
+ const ctx = {
3297
+ ...createAdapterContext(state.commsDir, repoRoot),
3298
+ instanceId,
3299
+ agentName: effectiveAgentName
3300
+ };
3131
3301
  const adapter = getAdapter(runtime);
3132
3302
  const warnings = [];
3133
3303
  log("Probing runtime...");
@@ -3215,14 +3385,8 @@ async function addCommand(args) {
3215
3385
  logWarn("Bridge script not found. Bridge not started.");
3216
3386
  warnings.push("Bridge script not found. Run bridge manually.");
3217
3387
  } else {
3218
- const resolvedAgentName = agentNameFlag || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
3219
- if (!resolvedAgentName) {
3220
- logWarn(
3221
- "No agent name set. Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
3222
- );
3223
- warnings.push("Bridge not auto-started: no agent name available.");
3224
- } else {
3225
- const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3388
+ const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3389
+ {
3226
3390
  log(`Starting bridge: ${bridgeScript}`);
3227
3391
  try {
3228
3392
  bridge = await startBridge({
@@ -3232,7 +3396,7 @@ async function addCommand(args) {
3232
3396
  commsDir: ctx.commsDir,
3233
3397
  bridgeScript,
3234
3398
  platform: ctx.platform,
3235
- agentName: resolvedAgentName,
3399
+ agentName: agentNameFlag ?? void 0,
3236
3400
  runtimeCommand: resolvedCfg.runtimeCommand,
3237
3401
  appServerUrl: resolvedCfg.appServerUrl,
3238
3402
  repoRoot,
@@ -3248,7 +3412,6 @@ async function addCommand(args) {
3248
3412
  }
3249
3413
  }
3250
3414
  }
3251
- const existingAgentName = state.instances[instanceId]?.agentName ?? null;
3252
3415
  const instanceState = {
3253
3416
  instanceId,
3254
3417
  runtime,
@@ -3978,7 +4141,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3978
4141
  log(`TUI connect: ${bridge.appServer.url}`);
3979
4142
  }
3980
4143
  }
3981
- const updated = { ...instance, bridge };
4144
+ const updated = { ...instance, bridge, manageAppServer, noAuth };
3982
4145
  const newState = updateInstanceState(state, instanceId, updated);
3983
4146
  saveState(repoRoot, newState);
3984
4147
  return {
@@ -4469,6 +4632,121 @@ function bridgeStatusOne(identifier) {
4469
4632
  }
4470
4633
  };
4471
4634
  }
4635
+ async function bridgeRestart(identifier, flags) {
4636
+ const repoRoot = findRepoRoot();
4637
+ const state = loadState(repoRoot);
4638
+ if (!state) {
4639
+ return {
4640
+ ok: false,
4641
+ command: "bridge",
4642
+ code: "TAP_NOT_INITIALIZED",
4643
+ message: "Not initialized. Run: npx @hua-labs/tap init",
4644
+ warnings: [],
4645
+ data: {}
4646
+ };
4647
+ }
4648
+ const resolved = resolveInstanceId(identifier, state);
4649
+ if (!resolved.ok) {
4650
+ return {
4651
+ ok: false,
4652
+ command: "bridge",
4653
+ code: resolved.code,
4654
+ message: resolved.message,
4655
+ warnings: [],
4656
+ data: {}
4657
+ };
4658
+ }
4659
+ const instanceId = resolved.instanceId;
4660
+ const inst = state.instances[instanceId];
4661
+ if (!inst) {
4662
+ return {
4663
+ ok: false,
4664
+ command: "bridge",
4665
+ code: "TAP_INSTANCE_NOT_FOUND",
4666
+ message: `Instance not found: ${instanceId}`,
4667
+ warnings: [],
4668
+ data: {}
4669
+ };
4670
+ }
4671
+ const adapter = getAdapter(inst.runtime);
4672
+ const ctx = {
4673
+ ...createAdapterContext(state.commsDir, repoRoot),
4674
+ instanceId
4675
+ };
4676
+ const bridgeScript = adapter.resolveBridgeScript?.(ctx);
4677
+ if (!bridgeScript) {
4678
+ return {
4679
+ ok: false,
4680
+ command: "bridge",
4681
+ instanceId,
4682
+ code: "TAP_BRIDGE_SCRIPT_MISSING",
4683
+ message: `Bridge script not found for ${instanceId}`,
4684
+ warnings: [],
4685
+ data: {}
4686
+ };
4687
+ }
4688
+ const { config: resolvedConfig } = resolveConfig({}, repoRoot);
4689
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
4690
+ const drainTimeout = parseInt(drainStr, 10) || 30;
4691
+ logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
4692
+ log(`Drain timeout: ${drainTimeout}s`);
4693
+ try {
4694
+ const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
4695
+ const { manageAppServer, noAuth } = inferRestartMode(
4696
+ currentBridgeState,
4697
+ {
4698
+ noServer: flags["no-server"] === true ? true : void 0,
4699
+ noAuth: flags["no-auth"] === true ? true : void 0
4700
+ },
4701
+ {
4702
+ manageAppServer: inst.manageAppServer,
4703
+ noAuth: inst.noAuth
4704
+ }
4705
+ );
4706
+ const bridge = await restartBridge({
4707
+ instanceId,
4708
+ runtime: inst.runtime,
4709
+ stateDir: ctx.stateDir,
4710
+ commsDir: ctx.commsDir,
4711
+ bridgeScript,
4712
+ platform: ctx.platform,
4713
+ agentName: inst.agentName ?? void 0,
4714
+ runtimeCommand: resolvedConfig.runtimeCommand,
4715
+ appServerUrl: resolvedConfig.appServerUrl,
4716
+ repoRoot,
4717
+ port: inst.port ?? void 0,
4718
+ headless: inst.headless,
4719
+ drainTimeoutSeconds: drainTimeout,
4720
+ manageAppServer,
4721
+ noAuth
4722
+ });
4723
+ logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
4724
+ const updated = { ...inst, bridge, manageAppServer, noAuth };
4725
+ const newState = updateInstanceState(state, instanceId, updated);
4726
+ saveState(repoRoot, newState);
4727
+ return {
4728
+ ok: true,
4729
+ command: "bridge",
4730
+ instanceId,
4731
+ code: "TAP_BRIDGE_START_OK",
4732
+ message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
4733
+ warnings: [],
4734
+ data: { pid: bridge.pid }
4735
+ };
4736
+ } catch (err) {
4737
+ const msg = err instanceof Error ? err.message : String(err);
4738
+ logError(msg);
4739
+ return {
4740
+ ok: false,
4741
+ command: "bridge",
4742
+ instanceId,
4743
+ code: "TAP_BRIDGE_START_FAILED",
4744
+ message: msg,
4745
+ warnings: [],
4746
+ data: {}
4747
+ };
4748
+ }
4749
+ }
4472
4750
  async function bridgeCommand(args) {
4473
4751
  const { positional, flags } = parseArgs(args);
4474
4752
  const subcommand = positional[0];
@@ -4528,12 +4806,25 @@ async function bridgeCommand(args) {
4528
4806
  }
4529
4807
  return bridgeStatusAll();
4530
4808
  }
4809
+ case "restart": {
4810
+ if (!identifierArg) {
4811
+ return {
4812
+ ok: false,
4813
+ command: "bridge",
4814
+ code: "TAP_INVALID_ARGUMENT",
4815
+ message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
4816
+ warnings: [],
4817
+ data: {}
4818
+ };
4819
+ }
4820
+ return bridgeRestart(identifierArg, flags);
4821
+ }
4531
4822
  default:
4532
4823
  return {
4533
4824
  ok: false,
4534
4825
  command: "bridge",
4535
4826
  code: "TAP_INVALID_ARGUMENT",
4536
- message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
4827
+ message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
4537
4828
  warnings: [],
4538
4829
  data: {}
4539
4830
  };
@@ -4543,7 +4834,7 @@ async function bridgeCommand(args) {
4543
4834
  // src/engine/dashboard.ts
4544
4835
  import * as fs15 from "fs";
4545
4836
  import * as path15 from "path";
4546
- import { execSync as execSync4 } from "child_process";
4837
+ import { execSync as execSync5 } from "child_process";
4547
4838
  function collectAgents(commsDir) {
4548
4839
  const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
4549
4840
  if (!fs15.existsSync(heartbeatsPath)) return [];
@@ -4622,7 +4913,7 @@ function collectBridges(repoRoot) {
4622
4913
  }
4623
4914
  function collectPRs() {
4624
4915
  try {
4625
- const output = execSync4(
4916
+ const output = execSync5(
4626
4917
  "gh pr list --state all --limit 10 --json number,title,author,state,url",
4627
4918
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
4628
4919
  );
@@ -4835,7 +5126,9 @@ async function serveCommand(args) {
4835
5126
  data: {}
4836
5127
  };
4837
5128
  }
4838
- const child = spawn2(managed.command, managed.args, {
5129
+ const serveCommand2 = managed.command === "npx" ? "node" : managed.command;
5130
+ const serveArgs = managed.command === "npx" && managed.sourcePath ? [managed.sourcePath] : managed.args;
5131
+ const child = spawn2(serveCommand2, serveArgs, {
4839
5132
  stdio: "inherit",
4840
5133
  env: {
4841
5134
  ...process.env,
@@ -4869,7 +5162,7 @@ async function serveCommand(args) {
4869
5162
  // src/commands/init-worktree.ts
4870
5163
  import * as fs16 from "fs";
4871
5164
  import * as path17 from "path";
4872
- import { execSync as execSync5 } from "child_process";
5165
+ import { execSync as execSync6 } from "child_process";
4873
5166
  var INIT_WORKTREE_HELP = `
4874
5167
  Usage:
4875
5168
  tap-comms init-worktree [options]
@@ -4893,7 +5186,7 @@ function warn(warnings, message) {
4893
5186
  }
4894
5187
  function run(cmd, opts) {
4895
5188
  try {
4896
- return execSync5(cmd, {
5189
+ return execSync6(cmd, {
4897
5190
  cwd: opts?.cwd,
4898
5191
  encoding: "utf-8",
4899
5192
  stdio: ["pipe", "pipe", "pipe"],
@@ -4910,7 +5203,7 @@ function toAbsolute(p) {
4910
5203
  }
4911
5204
  function probeBun(candidate) {
4912
5205
  try {
4913
- const out = execSync5(`"${candidate}" --version`, {
5206
+ const out = execSync6(`"${candidate}" --version`, {
4914
5207
  encoding: "utf-8",
4915
5208
  stdio: ["pipe", "pipe", "pipe"],
4916
5209
  timeout: 5e3
@@ -4924,7 +5217,7 @@ function findBun() {
4924
5217
  const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
4925
5218
  for (const name of candidates) {
4926
5219
  try {
4927
- const out = execSync5(
5220
+ const out = execSync6(
4928
5221
  process.platform === "win32" ? `where ${name}` : `which ${name}`,
4929
5222
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
4930
5223
  ).trim();
@@ -5376,11 +5669,12 @@ async function dashboardCommand(args) {
5376
5669
  import {
5377
5670
  existsSync as existsSync16,
5378
5671
  mkdirSync as mkdirSync10,
5379
- readdirSync as readdirSync3,
5672
+ readdirSync as readdirSync4,
5380
5673
  readFileSync as readFileSync14,
5381
5674
  statSync as statSync2,
5382
5675
  unlinkSync as unlinkSync3
5383
5676
  } from "fs";
5677
+ import { execSync as execSync7 } from "child_process";
5384
5678
  import { join as join17 } from "path";
5385
5679
  var PASS = "pass";
5386
5680
  var WARN = "warn";
@@ -5388,7 +5682,7 @@ var FAIL = "fail";
5388
5682
  function countFiles(dir, ext = ".md") {
5389
5683
  if (!existsSync16(dir)) return 0;
5390
5684
  try {
5391
- return readdirSync3(dir).filter((f) => f.endsWith(ext)).length;
5685
+ return readdirSync4(dir).filter((f) => f.endsWith(ext)).length;
5392
5686
  } catch {
5393
5687
  return 0;
5394
5688
  }
@@ -5398,7 +5692,7 @@ function recentFileCount(dir, withinMs) {
5398
5692
  const cutoff = Date.now() - withinMs;
5399
5693
  let count = 0;
5400
5694
  try {
5401
- for (const f of readdirSync3(dir)) {
5695
+ for (const f of readdirSync4(dir)) {
5402
5696
  if (!f.endsWith(".md")) continue;
5403
5697
  try {
5404
5698
  if (statSync2(join17(dir, f)).mtimeMs > cutoff) count++;
@@ -5409,6 +5703,21 @@ function recentFileCount(dir, withinMs) {
5409
5703
  }
5410
5704
  return count;
5411
5705
  }
5706
+ function loadBridgeRuntimeHeartbeat(bridgeState) {
5707
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
5708
+ if (!runtimeStateDir) {
5709
+ return null;
5710
+ }
5711
+ const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
5712
+ if (!existsSync16(heartbeatPath)) {
5713
+ return null;
5714
+ }
5715
+ try {
5716
+ return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
5717
+ } catch {
5718
+ return null;
5719
+ }
5720
+ }
5412
5721
  function checkComms(commsDir) {
5413
5722
  const checks = [];
5414
5723
  checks.push({
@@ -5492,6 +5801,7 @@ function checkInstances(repoRoot, stateDir) {
5492
5801
  const running = isBridgeRunning(stateDir, id);
5493
5802
  const bridgeState = loadBridgeState(stateDir, id);
5494
5803
  const heartbeatAge = getHeartbeatAge(stateDir, id);
5804
+ const runtimeHeartbeat = loadBridgeRuntimeHeartbeat(bridgeState);
5495
5805
  let status;
5496
5806
  let message;
5497
5807
  let fix;
@@ -5535,6 +5845,11 @@ function checkInstances(repoRoot, stateDir) {
5535
5845
  status = WARN;
5536
5846
  message = "Not running";
5537
5847
  }
5848
+ const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
5849
+ if (lastRuntimeError) {
5850
+ status = status === FAIL ? FAIL : WARN;
5851
+ message = `${message}; bridge last error: ${lastRuntimeError}`;
5852
+ }
5538
5853
  checks.push({ name: `bridge: ${id}`, status, message, fix });
5539
5854
  } else {
5540
5855
  checks.push({
@@ -5588,35 +5903,205 @@ function checkMessageLifecycle(commsDir) {
5588
5903
  function checkMcpServer(repoRoot) {
5589
5904
  const checks = [];
5590
5905
  const mcpJson = join17(repoRoot, ".mcp.json");
5591
- if (existsSync16(mcpJson)) {
5906
+ if (!existsSync16(mcpJson)) {
5907
+ checks.push({
5908
+ name: "MCP config (.mcp.json)",
5909
+ status: WARN,
5910
+ message: "Not found \u2014 MCP channel notifications won't work"
5911
+ });
5912
+ return checks;
5913
+ }
5914
+ let config;
5915
+ try {
5916
+ config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
5917
+ } catch {
5918
+ checks.push({
5919
+ name: "MCP config (.mcp.json)",
5920
+ status: WARN,
5921
+ message: "File exists but invalid JSON"
5922
+ });
5923
+ return checks;
5924
+ }
5925
+ const hasTapComms = config?.mcpServers?.["tap-comms"];
5926
+ if (!hasTapComms) {
5927
+ checks.push({
5928
+ name: "MCP config (.mcp.json)",
5929
+ status: WARN,
5930
+ message: "tap-comms not configured"
5931
+ });
5932
+ return checks;
5933
+ }
5934
+ checks.push({
5935
+ name: "MCP config (.mcp.json)",
5936
+ status: PASS,
5937
+ message: `command: ${hasTapComms.command}`
5938
+ });
5939
+ if (hasTapComms.command) {
5940
+ const cmd = hasTapComms.command;
5941
+ let cmdAvailable = existsSync16(cmd);
5942
+ if (!cmdAvailable) {
5943
+ try {
5944
+ execSync7(`"${cmd}" --version`, {
5945
+ stdio: "pipe",
5946
+ timeout: 5e3
5947
+ });
5948
+ cmdAvailable = true;
5949
+ } catch {
5950
+ }
5951
+ }
5952
+ checks.push({
5953
+ name: "MCP command binary",
5954
+ status: cmdAvailable ? PASS : FAIL,
5955
+ message: cmdAvailable ? cmd : `Not found: ${cmd} (checked PATH and absolute)`
5956
+ });
5957
+ }
5958
+ if (hasTapComms.args?.[0]) {
5959
+ const mcpScript = hasTapComms.args[0];
5960
+ checks.push({
5961
+ name: "MCP server script",
5962
+ status: existsSync16(mcpScript) ? PASS : FAIL,
5963
+ message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
5964
+ });
5965
+ if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
5966
+ checks.push({
5967
+ name: "MCP SQLite support",
5968
+ status: WARN,
5969
+ message: "Node + .mjs = no SQLite (bun:sqlite unavailable). Use bun or .ts source for full features."
5970
+ });
5971
+ }
5972
+ }
5973
+ if (!hasTapComms.cwd) {
5974
+ checks.push({
5975
+ name: "MCP cwd field",
5976
+ status: WARN,
5977
+ message: "No cwd in .mcp.json \u2014 worktree sessions may fail to resolve MCP server dependencies"
5978
+ });
5979
+ } else {
5980
+ checks.push({
5981
+ name: "MCP cwd field",
5982
+ status: PASS,
5983
+ message: hasTapComms.cwd
5984
+ });
5985
+ }
5986
+ const envCommsDir = hasTapComms.env?.TAP_COMMS_DIR;
5987
+ if (!envCommsDir) {
5988
+ checks.push({
5989
+ name: "MCP TAP_COMMS_DIR",
5990
+ status: FAIL,
5991
+ message: "TAP_COMMS_DIR not set in .mcp.json env \u2014 server will fail to start"
5992
+ });
5993
+ } else {
5994
+ checks.push({
5995
+ name: "MCP TAP_COMMS_DIR",
5996
+ status: existsSync16(envCommsDir) ? PASS : FAIL,
5997
+ message: existsSync16(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
5998
+ });
5999
+ }
6000
+ checks.push({
6001
+ name: "MCP session cache",
6002
+ status: PASS,
6003
+ message: "If .mcp.json was changed mid-session, restart Claude (Ctrl+C \u2192 claude --resume) to reload"
6004
+ });
6005
+ return checks;
6006
+ }
6007
+ function checkBridgeTurnHealth(repoRoot) {
6008
+ const checks = [];
6009
+ const tmpDir = join17(repoRoot, ".tmp");
6010
+ if (!existsSync16(tmpDir)) return checks;
6011
+ const state = loadState(repoRoot);
6012
+ const activeMatchers = /* @__PURE__ */ new Set();
6013
+ if (state) {
6014
+ for (const [id, inst] of Object.entries(state.instances)) {
6015
+ if (inst?.installed && inst.bridgeMode === "app-server") {
6016
+ activeMatchers.add(id);
6017
+ if (inst.agentName) activeMatchers.add(inst.agentName);
6018
+ }
6019
+ }
6020
+ }
6021
+ let dirs;
6022
+ try {
6023
+ dirs = readdirSync4(tmpDir).filter((d) => {
6024
+ if (!d.startsWith("codex-app-server-bridge")) return false;
6025
+ const suffix = d.replace("codex-app-server-bridge-", "");
6026
+ if (activeMatchers.size === 0) return true;
6027
+ for (const matcher of activeMatchers) {
6028
+ if (suffix === matcher || suffix.startsWith(matcher)) return true;
6029
+ }
6030
+ return false;
6031
+ });
6032
+ } catch {
6033
+ return checks;
6034
+ }
6035
+ for (const dir of dirs) {
6036
+ const heartbeatPath = join17(tmpDir, dir, "heartbeat.json");
6037
+ if (!existsSync16(heartbeatPath)) continue;
6038
+ let heartbeat;
5592
6039
  try {
5593
- const config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
5594
- const hasTapComms = config?.mcpServers?.["tap-comms"];
6040
+ heartbeat = JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
6041
+ } catch {
6042
+ checks.push({
6043
+ name: `turn: ${dir}`,
6044
+ status: WARN,
6045
+ message: "heartbeat.json unreadable"
6046
+ });
6047
+ continue;
6048
+ }
6049
+ const heartbeatAge = heartbeat.updatedAt ? Math.floor(
6050
+ (Date.now() - new Date(heartbeat.updatedAt).getTime()) / 1e3
6051
+ ) : null;
6052
+ if (heartbeat.connected === false || heartbeat.initialized === false) {
5595
6053
  checks.push({
5596
- name: "MCP config (.mcp.json)",
5597
- status: hasTapComms ? PASS : WARN,
5598
- message: hasTapComms ? `command: ${hasTapComms.command}` : "tap-comms not configured"
6054
+ name: `turn: ${dir}`,
6055
+ status: FAIL,
6056
+ message: `disconnected (connected=${heartbeat.connected}, initialized=${heartbeat.initialized})${heartbeat.lastError ? ` \u2014 ${heartbeat.lastError}` : ""}`
5599
6057
  });
5600
- if (hasTapComms?.args?.[0]) {
5601
- const mcpScript = hasTapComms.args[0];
6058
+ continue;
6059
+ }
6060
+ if (heartbeatAge !== null && heartbeatAge > 300) {
6061
+ checks.push({
6062
+ name: `turn: ${dir}`,
6063
+ status: FAIL,
6064
+ message: `dead \u2014 heartbeat ${Math.round(heartbeatAge)}s ago, no updates`
6065
+ });
6066
+ continue;
6067
+ }
6068
+ if (heartbeat.activeTurnId) {
6069
+ const ZOMBIE_THRESHOLD = 30 * 60;
6070
+ const lastNotifAge = heartbeat.lastNotificationAt ? Math.floor(
6071
+ (Date.now() - new Date(heartbeat.lastNotificationAt).getTime()) / 1e3
6072
+ ) : null;
6073
+ if (lastNotifAge !== null && lastNotifAge > ZOMBIE_THRESHOLD) {
5602
6074
  checks.push({
5603
- name: "MCP server script",
5604
- status: existsSync16(mcpScript) ? PASS : FAIL,
5605
- message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
6075
+ name: `turn: ${dir}`,
6076
+ status: WARN,
6077
+ message: `zombie \u2014 active turn ${heartbeat.activeTurnId}, last notification ${Math.round(lastNotifAge / 60)}m ago (${heartbeat.lastNotificationMethod ?? "?"}). MCP tools may not be exposed in app-server turns \u2014 try bridge restart${heartbeat.lastError ? `. Error: ${heartbeat.lastError}` : ""}`
5606
6078
  });
6079
+ continue;
5607
6080
  }
5608
- } catch {
6081
+ const failures2 = heartbeat.consecutiveFailureCount ?? 0;
6082
+ if (failures2 > 0 && heartbeatAge !== null && heartbeatAge < 60) {
6083
+ checks.push({
6084
+ name: `turn: ${dir}`,
6085
+ status: WARN,
6086
+ message: `zombie \u2014 active turn ${heartbeat.activeTurnId}, ${failures2} consecutive failures. MCP tools may not be exposed in app-server turns \u2014 try bridge restart${heartbeat.lastError ? `. Error: ${heartbeat.lastError}` : ""}`
6087
+ });
6088
+ continue;
6089
+ }
6090
+ }
6091
+ const failures = heartbeat.consecutiveFailureCount ?? 0;
6092
+ if (failures > 5) {
5609
6093
  checks.push({
5610
- name: "MCP config (.mcp.json)",
6094
+ name: `turn: ${dir}`,
5611
6095
  status: WARN,
5612
- message: "File exists but invalid JSON"
6096
+ message: `slow \u2014 ${failures} consecutive failures, last: ${heartbeat.lastError ?? "unknown"}`
5613
6097
  });
6098
+ continue;
5614
6099
  }
5615
- } else {
6100
+ const turnInfo = heartbeat.activeTurnId ? `active turn ${heartbeat.activeTurnId}` : `idle (last: ${heartbeat.lastTurnStatus ?? "none"})`;
5616
6101
  checks.push({
5617
- name: "MCP config (.mcp.json)",
5618
- status: WARN,
5619
- message: "Not found \u2014 MCP channel notifications won't work"
6102
+ name: `turn: ${dir}`,
6103
+ status: PASS,
6104
+ message: `healthy \u2014 ${turnInfo}, heartbeat ${heartbeatAge ?? "?"}s ago`
5620
6105
  });
5621
6106
  }
5622
6107
  return checks;
@@ -5655,10 +6140,17 @@ async function doctorCommand(args) {
5655
6140
  checks.push(...checkInstances(repoRoot, config.stateDir));
5656
6141
  checks.push(...checkMessageLifecycle(commsDir));
5657
6142
  checks.push(...checkMcpServer(repoRoot));
6143
+ checks.push(...checkBridgeTurnHealth(repoRoot));
5658
6144
  return checks;
5659
6145
  }
5660
6146
  const initialChecks = runAllChecks();
5661
- for (const section of ["Comms", "Instances", "Messages", "MCP"]) {
6147
+ for (const section of [
6148
+ "Comms",
6149
+ "Instances",
6150
+ "Messages",
6151
+ "MCP",
6152
+ "Turns"
6153
+ ]) {
5662
6154
  const sectionChecks = {
5663
6155
  Comms: initialChecks.filter(
5664
6156
  (c) => [
@@ -5677,7 +6169,8 @@ async function doctorCommand(args) {
5677
6169
  ),
5678
6170
  MCP: initialChecks.filter(
5679
6171
  (c) => c.name.startsWith("MCP") || c.name === "MCP server script"
5680
- )
6172
+ ),
6173
+ Turns: initialChecks.filter((c) => c.name.startsWith("turn:"))
5681
6174
  }[section];
5682
6175
  if (sectionChecks.length > 0) {
5683
6176
  log(`${section}:`);
@@ -5737,6 +6230,158 @@ async function doctorCommand(args) {
5737
6230
  };
5738
6231
  }
5739
6232
 
6233
+ // src/commands/comms.ts
6234
+ import { execSync as execSync8 } from "child_process";
6235
+ import * as fs17 from "fs";
6236
+ import * as path18 from "path";
6237
+ var COMMS_HELP = `
6238
+ Usage:
6239
+ tap-comms comms <subcommand>
6240
+
6241
+ Subcommands:
6242
+ pull Pull latest changes from comms remote repo
6243
+ push Commit and push comms changes to remote repo
6244
+
6245
+ Examples:
6246
+ npx @hua-labs/tap comms pull
6247
+ npx @hua-labs/tap comms push
6248
+ `.trim();
6249
+ function isGitRepo(dir) {
6250
+ return fs17.existsSync(path18.join(dir, ".git"));
6251
+ }
6252
+ function commsPull(commsDir) {
6253
+ logHeader("tap comms pull");
6254
+ if (!isGitRepo(commsDir)) {
6255
+ logError(`${commsDir} is not a git repository`);
6256
+ return {
6257
+ ok: false,
6258
+ command: "comms",
6259
+ code: "TAP_COMMS_NOT_REPO",
6260
+ message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
6261
+ warnings: [],
6262
+ data: { commsDir }
6263
+ };
6264
+ }
6265
+ try {
6266
+ const output = execSync8("git pull --rebase", {
6267
+ cwd: commsDir,
6268
+ encoding: "utf-8",
6269
+ stdio: "pipe"
6270
+ });
6271
+ logSuccess("Comms pull complete");
6272
+ if (output.trim()) log(output.trim());
6273
+ return {
6274
+ ok: true,
6275
+ command: "comms",
6276
+ code: "TAP_COMMS_PULL_OK",
6277
+ message: "Comms pull complete",
6278
+ warnings: [],
6279
+ data: { commsDir }
6280
+ };
6281
+ } catch (err) {
6282
+ const msg = err instanceof Error ? err.message : String(err);
6283
+ logError(`Pull failed: ${msg}`);
6284
+ return {
6285
+ ok: false,
6286
+ command: "comms",
6287
+ code: "TAP_COMMS_PULL_FAILED",
6288
+ message: `Pull failed: ${msg}`,
6289
+ warnings: [],
6290
+ data: { commsDir }
6291
+ };
6292
+ }
6293
+ }
6294
+ function commsPush(commsDir) {
6295
+ logHeader("tap comms push");
6296
+ if (!isGitRepo(commsDir)) {
6297
+ logError(`${commsDir} is not a git repository`);
6298
+ return {
6299
+ ok: false,
6300
+ command: "comms",
6301
+ code: "TAP_COMMS_NOT_REPO",
6302
+ message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
6303
+ warnings: [],
6304
+ data: { commsDir }
6305
+ };
6306
+ }
6307
+ try {
6308
+ execSync8("git add -A", { cwd: commsDir, stdio: "pipe" });
6309
+ const status = execSync8("git status --porcelain", {
6310
+ cwd: commsDir,
6311
+ encoding: "utf-8",
6312
+ stdio: "pipe"
6313
+ }).trim();
6314
+ if (!status) {
6315
+ log("Nothing to push \u2014 comms directory is clean");
6316
+ return {
6317
+ ok: true,
6318
+ command: "comms",
6319
+ code: "TAP_COMMS_PUSH_OK",
6320
+ message: "Nothing to push",
6321
+ warnings: [],
6322
+ data: { commsDir, changed: false }
6323
+ };
6324
+ }
6325
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6326
+ execSync8(`git commit -m "chore(comms): sync ${timestamp}"`, {
6327
+ cwd: commsDir,
6328
+ stdio: "pipe"
6329
+ });
6330
+ execSync8("git push", { cwd: commsDir, stdio: "pipe" });
6331
+ logSuccess("Comms push complete");
6332
+ return {
6333
+ ok: true,
6334
+ command: "comms",
6335
+ code: "TAP_COMMS_PUSH_OK",
6336
+ message: "Comms push complete",
6337
+ warnings: [],
6338
+ data: { commsDir, changed: true }
6339
+ };
6340
+ } catch (err) {
6341
+ const msg = err instanceof Error ? err.message : String(err);
6342
+ logError(`Push failed: ${msg}`);
6343
+ return {
6344
+ ok: false,
6345
+ command: "comms",
6346
+ code: "TAP_COMMS_PUSH_FAILED",
6347
+ message: `Push failed: ${msg}`,
6348
+ warnings: [],
6349
+ data: { commsDir }
6350
+ };
6351
+ }
6352
+ }
6353
+ async function commsCommand(args) {
6354
+ const subcommand = args[0];
6355
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
6356
+ log(COMMS_HELP);
6357
+ return {
6358
+ ok: true,
6359
+ command: "comms",
6360
+ code: "TAP_NO_OP",
6361
+ message: COMMS_HELP,
6362
+ warnings: [],
6363
+ data: {}
6364
+ };
6365
+ }
6366
+ const repoRoot = findRepoRoot();
6367
+ const commsDir = resolveCommsDir(args, repoRoot);
6368
+ switch (subcommand) {
6369
+ case "pull":
6370
+ return commsPull(commsDir);
6371
+ case "push":
6372
+ return commsPush(commsDir);
6373
+ default:
6374
+ return {
6375
+ ok: false,
6376
+ command: "comms",
6377
+ code: "TAP_INVALID_ARGUMENT",
6378
+ message: `Unknown comms subcommand: ${subcommand}. Use pull or push.`,
6379
+ warnings: [],
6380
+ data: {}
6381
+ };
6382
+ }
6383
+ }
6384
+
5740
6385
  // src/output.ts
5741
6386
  function emitResult(result, jsonMode) {
5742
6387
  if (jsonMode) {
@@ -5777,6 +6422,7 @@ Commands:
5777
6422
  bridge <sub> [inst] Manage bridges (start, stop, status)
5778
6423
  up Start all registered bridge daemons
5779
6424
  down Stop all running bridge daemons
6425
+ comms <pull|push> Sync comms directory with remote repo
5780
6426
  dashboard Show unified ops dashboard
5781
6427
  doctor Diagnose tap infrastructure health
5782
6428
  serve Start tap-comms MCP server (stdio)
@@ -5804,6 +6450,7 @@ function normalizeCommandName(command) {
5804
6450
  case "bridge":
5805
6451
  case "up":
5806
6452
  case "down":
6453
+ case "comms":
5807
6454
  case "dashboard":
5808
6455
  case "doctor":
5809
6456
  case "serve":
@@ -5861,6 +6508,9 @@ async function main() {
5861
6508
  case "down":
5862
6509
  result = await downCommand(commandArgs);
5863
6510
  break;
6511
+ case "comms":
6512
+ result = await commsCommand(commandArgs);
6513
+ break;
5864
6514
  case "dashboard":
5865
6515
  result = await dashboardCommand(commandArgs);
5866
6516
  break;