@hua-labs/tap 0.2.3 → 0.2.5

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,7 +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
+ import { spawnSync } from "child_process";
5
5
 
6
6
  // src/state.ts
7
7
  import * as fs3 from "fs";
@@ -23,9 +23,16 @@ function detectPlatform() {
23
23
  return process.platform;
24
24
  }
25
25
  var _noGitWarned = false;
26
+ var _loggedWarnings = /* @__PURE__ */ new Set();
26
27
  function _setNoGitWarned() {
27
28
  _noGitWarned = true;
28
29
  }
30
+ function resetLoggedWarnings() {
31
+ _loggedWarnings.clear();
32
+ }
33
+ function wasWarningLogged(message) {
34
+ return _loggedWarnings.has(message);
35
+ }
29
36
  function findRepoRoot(startDir = process.cwd()) {
30
37
  let dir = path.resolve(startDir);
31
38
  while (true) {
@@ -33,8 +40,8 @@ function findRepoRoot(startDir = process.cwd()) {
33
40
  if (fs.existsSync(path.join(dir, "package.json"))) {
34
41
  if (!_noGitWarned) {
35
42
  _setNoGitWarned();
36
- logWarn(
37
- "No .git directory found. Resolved repo root via package.json \u2014 comms directory may be created in an unexpected location. Use --comms-dir to specify explicitly."
43
+ log(
44
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
38
45
  );
39
46
  }
40
47
  return dir;
@@ -45,8 +52,8 @@ function findRepoRoot(startDir = process.cwd()) {
45
52
  }
46
53
  if (!_noGitWarned) {
47
54
  _setNoGitWarned();
48
- logWarn(
49
- "No git repository or package.json found. Using current directory as root. Run 'git init' first, or use --comms-dir to specify the comms path."
55
+ log(
56
+ "No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
50
57
  );
51
58
  }
52
59
  return process.cwd();
@@ -101,7 +108,9 @@ function logSuccess(message) {
101
108
  if (!_jsonMode) console.log(` + ${message}`);
102
109
  }
103
110
  function logWarn(message) {
104
- if (!_jsonMode) console.log(` ! ${message}`);
111
+ if (_jsonMode) return;
112
+ _loggedWarnings.add(message);
113
+ console.log(` ! ${message}`);
105
114
  }
106
115
  function logError(message) {
107
116
  if (!_jsonMode) console.error(` x ${message}`);
@@ -111,6 +120,16 @@ function logHeader(message) {
111
120
  ${message}
112
121
  `);
113
122
  }
123
+ function parseIntFlag(value, name, min, max) {
124
+ if (value === void 0) return void 0;
125
+ const parsed = Number(value);
126
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
127
+ throw new RangeError(
128
+ `Invalid ${name}: ${value}. Must be an integer between ${min} and ${max}.`
129
+ );
130
+ }
131
+ return parsed;
132
+ }
114
133
  function resolveInstanceId(identifier, state) {
115
134
  if (state.instances[identifier]) {
116
135
  return { ok: true, instanceId: identifier };
@@ -160,8 +179,8 @@ function findRepoRoot2(startDir = process.cwd()) {
160
179
  if (fs2.existsSync(path2.join(dir, "package.json"))) {
161
180
  if (!_noGitWarned) {
162
181
  _setNoGitWarned();
163
- console.error(
164
- "[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
182
+ log(
183
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
165
184
  );
166
185
  }
167
186
  return dir;
@@ -172,8 +191,8 @@ function findRepoRoot2(startDir = process.cwd()) {
172
191
  }
173
192
  if (!_noGitWarned) {
174
193
  _setNoGitWarned();
175
- console.error(
176
- "[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
194
+ log(
195
+ "No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
177
196
  );
178
197
  }
179
198
  return process.cwd();
@@ -187,7 +206,7 @@ function loadJsonFile(filePath) {
187
206
  return null;
188
207
  }
189
208
  }
190
- function loadSharedConfig2(repoRoot) {
209
+ function loadSharedConfig(repoRoot) {
191
210
  return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
192
211
  }
193
212
  function loadLocalConfig(repoRoot) {
@@ -211,7 +230,7 @@ function loadLegacyShellConfig(repoRoot) {
211
230
  }
212
231
  function resolveConfig(overrides = {}, startDir) {
213
232
  const repoRoot = findRepoRoot2(startDir);
214
- const shared = loadSharedConfig2(repoRoot) ?? {};
233
+ const shared = loadSharedConfig(repoRoot) ?? {};
215
234
  const local = loadLocalConfig(repoRoot) ?? {};
216
235
  const legacy = loadLegacyShellConfig(repoRoot) ?? {};
217
236
  const sources = {
@@ -754,7 +773,39 @@ function parsePermissionMode(args) {
754
773
  }
755
774
  return "safe";
756
775
  }
776
+ var INIT_HELP = `
777
+ Usage:
778
+ tap init [options]
779
+
780
+ Description:
781
+ Initialize the tap directory structure, state file, and permissions.
782
+ Optionally clone a shared comms repository.
783
+
784
+ Options:
785
+ --comms-dir <path> Override comms directory (default: tap-comms/)
786
+ --comms-repo <url> Clone a shared comms git repo into comms directory
787
+ --permissions <mode> Permission mode: safe (default) or full
788
+ --force Re-initialize even if already set up
789
+ --help, -h Show help
790
+
791
+ Examples:
792
+ npx @hua-labs/tap init
793
+ npx @hua-labs/tap init --permissions full
794
+ npx @hua-labs/tap init --comms-repo https://github.com/org/comms.git
795
+ npx @hua-labs/tap init --comms-dir /shared/comms --force
796
+ `.trim();
757
797
  async function initCommand(args) {
798
+ if (args.includes("--help") || args.includes("-h")) {
799
+ log(INIT_HELP);
800
+ return {
801
+ ok: true,
802
+ command: "init",
803
+ code: "TAP_NO_OP",
804
+ message: INIT_HELP,
805
+ warnings: [],
806
+ data: {}
807
+ };
808
+ }
758
809
  const repoRoot = findRepoRoot();
759
810
  const commsDir = resolveCommsDir(args, repoRoot);
760
811
  const permMode = parsePermissionMode(args);
@@ -791,10 +842,19 @@ async function initCommand(args) {
791
842
  } else {
792
843
  log(`Cloning comms repo: ${commsRepoUrl}`);
793
844
  try {
794
- execSync(`git clone "${commsRepoUrl}" "${commsDir}"`, {
795
- stdio: "pipe",
796
- encoding: "utf-8"
797
- });
845
+ const cloneResult = spawnSync(
846
+ "git",
847
+ ["clone", commsRepoUrl, commsDir],
848
+ {
849
+ stdio: "pipe",
850
+ encoding: "utf-8"
851
+ }
852
+ );
853
+ if (cloneResult.status !== 0) {
854
+ throw new Error(
855
+ cloneResult.stderr || `git clone exited with code ${cloneResult.status}`
856
+ );
857
+ }
798
858
  logSuccess(`Cloned comms repo to ${commsDir}`);
799
859
  } catch (err) {
800
860
  const msg = err instanceof Error ? err.message : String(err);
@@ -811,7 +871,7 @@ async function initCommand(args) {
811
871
  }
812
872
  }
813
873
  {
814
- const sharedConfig = loadSharedConfig2(repoRoot) ?? {};
874
+ const sharedConfig = loadSharedConfig(repoRoot) ?? {};
815
875
  let configChanged = false;
816
876
  if (commsRepoUrl) {
817
877
  sharedConfig.commsRepoUrl = commsRepoUrl;
@@ -903,17 +963,17 @@ ${entry}
903
963
  // src/adapters/claude.ts
904
964
  import * as fs8 from "fs";
905
965
  import * as path8 from "path";
906
- import { execSync as execSync2 } from "child_process";
966
+ import { execSync } from "child_process";
907
967
 
908
968
  // src/adapters/common.ts
909
969
  import * as fs7 from "fs";
910
970
  import * as os2 from "os";
911
971
  import * as path7 from "path";
912
- import { spawnSync } from "child_process";
972
+ import { spawnSync as spawnSync2 } from "child_process";
913
973
  import { fileURLToPath as fileURLToPath2 } from "url";
914
974
  function probeCommand(candidates) {
915
975
  for (const candidate of candidates) {
916
- const result = spawnSync(candidate, ["--version"], {
976
+ const result = spawnSync2(candidate, ["--version"], {
917
977
  encoding: "utf-8",
918
978
  shell: process.platform === "win32"
919
979
  });
@@ -991,7 +1051,7 @@ function findPreferredBunCommand() {
991
1051
  const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
992
1052
  for (const candidate of candidates) {
993
1053
  if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
994
- const result = spawnSync(candidate, ["--version"], {
1054
+ const result = spawnSync2(candidate, ["--version"], {
995
1055
  encoding: "utf-8",
996
1056
  shell: process.platform === "win32"
997
1057
  });
@@ -1017,7 +1077,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1017
1077
  }
1018
1078
  if (!sourcePath) {
1019
1079
  issues.push(
1020
- "tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
1080
+ "tap MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
1021
1081
  );
1022
1082
  return { command: null, args: [], env, sourcePath, warnings, issues };
1023
1083
  }
@@ -1047,7 +1107,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1047
1107
  }
1048
1108
  if (!command) {
1049
1109
  issues.push(
1050
- "bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
1110
+ "bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
1051
1111
  );
1052
1112
  return { command: null, args: [], env, sourcePath, warnings, issues };
1053
1113
  }
@@ -1062,13 +1122,14 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1062
1122
  }
1063
1123
 
1064
1124
  // src/adapters/claude.ts
1065
- var MCP_SERVER_KEY = "tap-comms";
1125
+ var MCP_SERVER_KEY = "tap";
1126
+ var OLD_MCP_SERVER_KEY = "tap-comms";
1066
1127
  function findMcpJsonPath(ctx) {
1067
1128
  return path8.join(ctx.repoRoot, ".mcp.json");
1068
1129
  }
1069
1130
  function findClaudeCommand() {
1070
1131
  try {
1071
- execSync2("claude --version", { stdio: "pipe" });
1132
+ execSync("claude --version", { stdio: "pipe" });
1072
1133
  return "claude";
1073
1134
  } catch {
1074
1135
  return null;
@@ -1140,6 +1201,11 @@ var claudeAdapter = {
1140
1201
  `Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
1141
1202
  );
1142
1203
  }
1204
+ if (config.mcpServers?.[OLD_MCP_SERVER_KEY]) {
1205
+ conflicts.push(
1206
+ `Legacy "${OLD_MCP_SERVER_KEY}" entry will be migrated to "${MCP_SERVER_KEY}".`
1207
+ );
1208
+ }
1143
1209
  } catch {
1144
1210
  warnings.push(
1145
1211
  ".mcp.json exists but is not valid JSON. Will be overwritten."
@@ -1149,7 +1215,7 @@ var claudeAdapter = {
1149
1215
  const serverEntry = buildMcpServerEntry(ctx);
1150
1216
  if (!serverEntry) {
1151
1217
  warnings.push(
1152
- "tap-comms MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
1218
+ "tap MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
1153
1219
  );
1154
1220
  return {
1155
1221
  runtime: "claude",
@@ -1202,6 +1268,10 @@ var claudeAdapter = {
1202
1268
  );
1203
1269
  }
1204
1270
  }
1271
+ const servers = config.mcpServers;
1272
+ if (servers?.[OLD_MCP_SERVER_KEY]) {
1273
+ delete servers[OLD_MCP_SERVER_KEY];
1274
+ }
1205
1275
  if (op.key) {
1206
1276
  setNestedKey(config, op.key, op.value);
1207
1277
  }
@@ -1250,7 +1320,7 @@ var claudeAdapter = {
1250
1320
  checks.push({ name: "Config is valid JSON", passed: true });
1251
1321
  const entry = config.mcpServers?.[MCP_SERVER_KEY];
1252
1322
  checks.push({
1253
- name: "tap-comms entry present",
1323
+ name: "tap entry present",
1254
1324
  passed: !!entry,
1255
1325
  message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
1256
1326
  });
@@ -1343,8 +1413,10 @@ function readArtifactBackup(backupPath) {
1343
1413
  }
1344
1414
 
1345
1415
  // src/adapters/codex.ts
1346
- var MCP_SELECTOR = "mcp_servers.tap-comms";
1347
- var ENV_SELECTOR = "mcp_servers.tap-comms.env";
1416
+ var MCP_SELECTOR = "mcp_servers.tap";
1417
+ var ENV_SELECTOR = "mcp_servers.tap.env";
1418
+ var OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
1419
+ var OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
1348
1420
  function findCodexConfigPath2() {
1349
1421
  return path10.join(getHomeDir(), ".codex", "config.toml");
1350
1422
  }
@@ -1398,12 +1470,12 @@ function verifyManagedToml(content, ctx, configPath) {
1398
1470
  message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1399
1471
  });
1400
1472
  checks.push({
1401
- name: "tap-comms MCP table present",
1473
+ name: "tap MCP table present",
1402
1474
  passed: !!mainTable,
1403
1475
  message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
1404
1476
  });
1405
1477
  checks.push({
1406
- name: "tap-comms env table present",
1478
+ name: "tap env table present",
1407
1479
  passed: !!envTable,
1408
1480
  message: envTable ? void 0 : `${ENV_SELECTOR} not found`
1409
1481
  });
@@ -1423,7 +1495,7 @@ function verifyManagedToml(content, ctx, configPath) {
1423
1495
  passed: mainTable.includes(
1424
1496
  `command = "${managed.command.replace(/\\/g, "\\\\")}"`
1425
1497
  ) && mainTable.includes(`args = [${expectedArgs}]`),
1426
- message: "Managed tap-comms command/args do not match expected values"
1498
+ message: "Managed tap command/args do not match expected values"
1427
1499
  });
1428
1500
  }
1429
1501
  return checks;
@@ -1473,6 +1545,11 @@ var codexAdapter = {
1473
1545
  if (extractTomlTable(content, MCP_SELECTOR)) {
1474
1546
  conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
1475
1547
  }
1548
+ if (extractTomlTable(content, OLD_MCP_SELECTOR)) {
1549
+ conflicts.push(
1550
+ `Legacy ${OLD_MCP_SELECTOR} table will be migrated to ${MCP_SELECTOR}.`
1551
+ );
1552
+ }
1476
1553
  if (extractTomlTable(content, ENV_SELECTOR)) {
1477
1554
  conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
1478
1555
  }
@@ -1538,6 +1615,12 @@ var codexAdapter = {
1538
1615
  return { ...artifact, backupPath };
1539
1616
  });
1540
1617
  let nextContent = existingContent;
1618
+ if (extractTomlTable(nextContent, OLD_ENV_SELECTOR)) {
1619
+ nextContent = removeTomlTable(nextContent, OLD_ENV_SELECTOR);
1620
+ }
1621
+ if (extractTomlTable(nextContent, OLD_MCP_SELECTOR)) {
1622
+ nextContent = removeTomlTable(nextContent, OLD_MCP_SELECTOR);
1623
+ }
1541
1624
  nextContent = replaceTomlTable(
1542
1625
  nextContent,
1543
1626
  MCP_SELECTOR,
@@ -1651,7 +1734,8 @@ var codexAdapter = {
1651
1734
  // src/adapters/gemini.ts
1652
1735
  import * as fs11 from "fs";
1653
1736
  import * as path11 from "path";
1654
- var GEMINI_SELECTOR = "mcpServers.tap-comms";
1737
+ var GEMINI_SELECTOR = "mcpServers.tap";
1738
+ var OLD_GEMINI_SELECTOR = "mcpServers.tap-comms";
1655
1739
  function candidateConfigPaths(ctx) {
1656
1740
  const home = getHomeDir();
1657
1741
  return [
@@ -1713,7 +1797,7 @@ function verifyGeminiConfig(config, configPath, ctx) {
1713
1797
  message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
1714
1798
  });
1715
1799
  checks.push({
1716
- name: "tap-comms entry present",
1800
+ name: "tap entry present",
1717
1801
  passed: !!entry,
1718
1802
  message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
1719
1803
  });
@@ -1779,6 +1863,11 @@ var geminiAdapter = {
1779
1863
  if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
1780
1864
  conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
1781
1865
  }
1866
+ if (readNestedKey(config, OLD_GEMINI_SELECTOR) !== void 0) {
1867
+ conflicts.push(
1868
+ `Legacy ${OLD_GEMINI_SELECTOR} entry will be migrated to ${GEMINI_SELECTOR}.`
1869
+ );
1870
+ }
1782
1871
  } catch {
1783
1872
  warnings.push(
1784
1873
  `${configPath} exists but is not valid JSON. It will be replaced.`
@@ -1846,6 +1935,13 @@ var geminiAdapter = {
1846
1935
  existed: previousValue !== void 0,
1847
1936
  value: previousValue
1848
1937
  });
1938
+ const oldValue = readNestedKey(config, OLD_GEMINI_SELECTOR);
1939
+ if (oldValue !== void 0) {
1940
+ const servers = config.mcpServers;
1941
+ if (servers) {
1942
+ delete servers["tap-comms"];
1943
+ }
1944
+ }
1849
1945
  setNestedKey2(config, GEMINI_SELECTOR, {
1850
1946
  command: managed.command,
1851
1947
  args: managed.args,
@@ -1927,15 +2023,16 @@ function getAdapter(runtime) {
1927
2023
  // src/engine/bridge.ts
1928
2024
  import * as fs13 from "fs";
1929
2025
  import * as net from "net";
2026
+ import * as os3 from "os";
1930
2027
  import * as path13 from "path";
1931
2028
  import { randomBytes } from "crypto";
1932
- import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
2029
+ import { spawn, spawnSync as spawnSync3, execSync as execSync3 } from "child_process";
1933
2030
  import { fileURLToPath as fileURLToPath4 } from "url";
1934
2031
 
1935
2032
  // src/runtime/resolve-node.ts
1936
2033
  import * as fs12 from "fs";
1937
2034
  import * as path12 from "path";
1938
- import { execSync as execSync3 } from "child_process";
2035
+ import { execSync as execSync2 } from "child_process";
1939
2036
  function readNodeVersion(repoRoot) {
1940
2037
  const nvFile = path12.join(repoRoot, ".node-version");
1941
2038
  if (!fs12.existsSync(nvFile)) return null;
@@ -1978,7 +2075,7 @@ function probeFnmNode(desiredVersion) {
1978
2075
  );
1979
2076
  if (!fs12.existsSync(candidate)) continue;
1980
2077
  try {
1981
- const v = execSync3(`"${candidate}" --version`, {
2078
+ const v = execSync2(`"${candidate}" --version`, {
1982
2079
  encoding: "utf-8",
1983
2080
  timeout: 5e3
1984
2081
  }).trim();
@@ -1992,7 +2089,7 @@ function probeFnmNode(desiredVersion) {
1992
2089
  }
1993
2090
  function detectNodeMajorVersion(command) {
1994
2091
  try {
1995
- const version2 = execSync3(`"${command}" --version`, {
2092
+ const version2 = execSync2(`"${command}" --version`, {
1996
2093
  encoding: "utf-8",
1997
2094
  timeout: 5e3
1998
2095
  }).trim();
@@ -2006,7 +2103,7 @@ function checkStripTypesSupport(command) {
2006
2103
  const major = detectNodeMajorVersion(command);
2007
2104
  if (major !== null && major >= 22) return true;
2008
2105
  try {
2009
- execSync3(`"${command}" --experimental-strip-types -e ""`, {
2106
+ execSync2(`"${command}" --experimental-strip-types -e ""`, {
2010
2107
  timeout: 5e3,
2011
2108
  stdio: "pipe"
2012
2109
  });
@@ -2097,8 +2194,10 @@ var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
2097
2194
  var APP_SERVER_START_TIMEOUT_MS = 2e4;
2098
2195
  var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
2099
2196
  var APP_SERVER_HEALTH_RETRY_MS = 250;
2100
- var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
2197
+ var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
2101
2198
  var APP_SERVER_AUTH_FILE_MODE = 384;
2199
+ var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
2200
+ var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
2102
2201
  function appServerLogFilePath(stateDir, instanceId) {
2103
2202
  return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
2104
2203
  }
@@ -2135,12 +2234,66 @@ function removeFileIfExists(filePath) {
2135
2234
  } catch {
2136
2235
  }
2137
2236
  }
2237
+ function toPowerShellSingleQuotedString(value) {
2238
+ return `'${value.replace(/'/g, "''")}'`;
2239
+ }
2240
+ function toPowerShellStringArrayLiteral(values) {
2241
+ return `@(${values.map(toPowerShellSingleQuotedString).join(", ")})`;
2242
+ }
2243
+ function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
2244
+ let entries;
2245
+ try {
2246
+ entries = fs13.readdirSync(os3.tmpdir());
2247
+ } catch {
2248
+ return;
2249
+ }
2250
+ for (const entry of entries) {
2251
+ if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
2252
+ continue;
2253
+ }
2254
+ const wrapperPath = path13.join(os3.tmpdir(), entry);
2255
+ try {
2256
+ const stats = fs13.statSync(wrapperPath);
2257
+ if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
2258
+ continue;
2259
+ }
2260
+ fs13.unlinkSync(wrapperPath);
2261
+ } catch {
2262
+ }
2263
+ }
2264
+ }
2265
+ function buildWindowsDetachedWrapperScript(command, args, logPath, stderrLogPath, env) {
2266
+ const lines = ["$ErrorActionPreference = 'Stop'"];
2267
+ for (const [key, value] of Object.entries(env)) {
2268
+ if (value !== void 0 && value !== process.env[key]) {
2269
+ lines.push(
2270
+ `[Environment]::SetEnvironmentVariable(${toPowerShellSingleQuotedString(key)}, ${toPowerShellSingleQuotedString(value)}, 'Process')`
2271
+ );
2272
+ }
2273
+ }
2274
+ lines.push(
2275
+ `$logPath = ${toPowerShellSingleQuotedString(logPath)}`,
2276
+ `$stderrLogPath = ${toPowerShellSingleQuotedString(stderrLogPath)}`,
2277
+ `$commandPath = ${toPowerShellSingleQuotedString(command)}`,
2278
+ `$commandArgs = ${toPowerShellStringArrayLiteral(args)}`,
2279
+ "$exitCode = 1",
2280
+ "try {",
2281
+ " & $commandPath @commandArgs >> $logPath 2>> $stderrLogPath",
2282
+ " $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }",
2283
+ "} finally {",
2284
+ " Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue",
2285
+ "}",
2286
+ "exit $exitCode"
2287
+ );
2288
+ return `${lines.join("\r\n")}\r
2289
+ `;
2290
+ }
2138
2291
  function getWebSocketCtor() {
2139
2292
  const candidate = globalThis.WebSocket;
2140
2293
  return typeof candidate === "function" ? candidate : null;
2141
2294
  }
2142
2295
  function delay(ms) {
2143
- return new Promise((resolve11) => setTimeout(resolve11, ms));
2296
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
2144
2297
  }
2145
2298
  function isLoopbackHost(hostname) {
2146
2299
  return hostname === "127.0.0.1" || hostname === "localhost";
@@ -2158,8 +2311,11 @@ function resolvePowerShellCommand() {
2158
2311
  function resolveAuthGatewayScript(repoRoot) {
2159
2312
  const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
2160
2313
  const candidates = [
2161
- path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
2162
- path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
2314
+ // Bundled: dist/bridges/ sibling (npm install / built package)
2315
+ path13.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
2316
+ // Source: src/bridges/ sibling (monorepo dev with ts runner)
2317
+ path13.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
2318
+ // Monorepo dist fallback
2163
2319
  path13.join(
2164
2320
  repoRoot,
2165
2321
  "packages",
@@ -2189,7 +2345,7 @@ function getBridgeRuntimeStateDir(repoRoot, instanceId) {
2189
2345
  }
2190
2346
  async function allocateLoopbackPort(hostname) {
2191
2347
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2192
- return await new Promise((resolve11, reject) => {
2348
+ return await new Promise((resolve13, reject) => {
2193
2349
  const server = net.createServer();
2194
2350
  server.unref();
2195
2351
  server.once("error", reject);
@@ -2207,15 +2363,13 @@ async function allocateLoopbackPort(hostname) {
2207
2363
  reject(error);
2208
2364
  return;
2209
2365
  }
2210
- resolve11(port);
2366
+ resolve13(port);
2211
2367
  });
2212
2368
  });
2213
2369
  });
2214
2370
  }
2215
- function buildProtectedAppServerUrl(publicUrl, token) {
2216
- const url = new URL(publicUrl);
2217
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
2218
- return url.toString().replace(/\/(?=\?|$)/, "");
2371
+ function buildProtectedAppServerUrl(publicUrl, _token) {
2372
+ return publicUrl;
2219
2373
  }
2220
2374
  function readGatewayTokenFromPath(tokenPath) {
2221
2375
  return fs13.readFileSync(tokenPath, "utf8").trim();
@@ -2330,7 +2484,7 @@ async function createManagedAppServerAuth(options) {
2330
2484
  throw new Error("Failed to spawn app-server auth gateway");
2331
2485
  }
2332
2486
  return {
2333
- mode: "query-token",
2487
+ mode: "subprotocol",
2334
2488
  protectedUrl,
2335
2489
  upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
2336
2490
  tokenPath,
@@ -2392,35 +2546,50 @@ function findReusableManagedAppServer(stateDir, publicUrl) {
2392
2546
  return null;
2393
2547
  }
2394
2548
  function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
2395
- const ext = path13.extname(command).toLowerCase();
2396
2549
  const stderrLogPath = stderrLogFilePath(logPath);
2397
- const stdoutFd = fs13.openSync(logPath, "a");
2398
- const stderrFd = fs13.openSync(stderrLogPath, "a");
2399
- try {
2400
- const child = ext === ".ps1" ? spawn(
2401
- resolvePowerShellCommand(),
2402
- ["-NoLogo", "-NoProfile", "-File", command, ...args],
2403
- {
2404
- cwd: repoRoot,
2405
- detached: true,
2406
- stdio: ["ignore", stdoutFd, stderrFd],
2407
- env,
2408
- windowsHide: true
2409
- }
2410
- ) : spawn(command, args, {
2411
- cwd: repoRoot,
2412
- detached: true,
2413
- stdio: ["ignore", stdoutFd, stderrFd],
2414
- env,
2415
- windowsHide: true,
2416
- shell: ext === ".cmd" || ext === ".bat"
2417
- });
2418
- child.unref();
2419
- return child.pid ?? null;
2420
- } finally {
2421
- fs13.closeSync(stdoutFd);
2422
- fs13.closeSync(stderrFd);
2550
+ const powerShellCommand = resolvePowerShellCommand();
2551
+ cleanupStaleWindowsSpawnWrappers();
2552
+ const wrapperPath = path13.join(
2553
+ os3.tmpdir(),
2554
+ `${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
2555
+ );
2556
+ fs13.writeFileSync(
2557
+ wrapperPath,
2558
+ buildWindowsDetachedWrapperScript(
2559
+ command,
2560
+ args,
2561
+ logPath,
2562
+ stderrLogPath,
2563
+ env
2564
+ )
2565
+ );
2566
+ const psCommand = [
2567
+ "$p = Start-Process",
2568
+ `-FilePath ${toPowerShellSingleQuotedString(powerShellCommand)}`,
2569
+ `-ArgumentList ${toPowerShellStringArrayLiteral(["-NoLogo", "-NoProfile", "-File", wrapperPath])}`,
2570
+ `-WorkingDirectory ${toPowerShellSingleQuotedString(repoRoot)}`,
2571
+ "-WindowStyle Hidden",
2572
+ "-PassThru",
2573
+ "; Write-Output $p.Id"
2574
+ ].join(" ");
2575
+ const result = spawnSync3(
2576
+ powerShellCommand,
2577
+ ["-NoLogo", "-NoProfile", "-Command", psCommand],
2578
+ {
2579
+ encoding: "utf-8",
2580
+ windowsHide: true
2581
+ }
2582
+ );
2583
+ if (result.status !== 0) {
2584
+ removeFileIfExists(wrapperPath);
2585
+ return null;
2423
2586
  }
2587
+ const pid = parseInt(result.stdout.trim(), 10);
2588
+ if (!Number.isFinite(pid)) {
2589
+ removeFileIfExists(wrapperPath);
2590
+ return null;
2591
+ }
2592
+ return pid;
2424
2593
  }
2425
2594
  function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
2426
2595
  return startWindowsDetachedProcess(
@@ -2444,7 +2613,7 @@ function findListeningProcessId(url, platform) {
2444
2613
  if (port == null || !Number.isFinite(port)) {
2445
2614
  return null;
2446
2615
  }
2447
- const result = spawnSync2(
2616
+ const result = spawnSync3(
2448
2617
  resolvePowerShellCommand(),
2449
2618
  [
2450
2619
  "-NoLogo",
@@ -2482,12 +2651,12 @@ function resolveAppServerUrl(baseUrl, port) {
2482
2651
  }
2483
2652
  async function isTcpPortAvailable(hostname, port) {
2484
2653
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2485
- return await new Promise((resolve11) => {
2654
+ return await new Promise((resolve13) => {
2486
2655
  const server = net.createServer();
2487
2656
  server.unref();
2488
- server.once("error", () => resolve11(false));
2657
+ server.once("error", () => resolve13(false));
2489
2658
  server.listen(port, bindHost, () => {
2490
- server.close((error) => resolve11(!error));
2659
+ server.close((error) => resolve13(!error));
2491
2660
  });
2492
2661
  });
2493
2662
  }
@@ -2517,12 +2686,12 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
2517
2686
  `Failed to find a free app-server port starting at ${basePort}`
2518
2687
  );
2519
2688
  }
2520
- async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
2689
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
2521
2690
  const WebSocket = getWebSocketCtor();
2522
2691
  if (!WebSocket) {
2523
2692
  return false;
2524
2693
  }
2525
- return new Promise((resolve11) => {
2694
+ return new Promise((resolve13) => {
2526
2695
  let settled = false;
2527
2696
  let socket = null;
2528
2697
  const finish = (healthy) => {
@@ -2535,11 +2704,12 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2535
2704
  socket?.close();
2536
2705
  } catch {
2537
2706
  }
2538
- resolve11(healthy);
2707
+ resolve13(healthy);
2539
2708
  };
2540
2709
  const timer = setTimeout(() => finish(false), timeoutMs);
2541
2710
  try {
2542
- socket = new WebSocket(url);
2711
+ const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
2712
+ socket = new WebSocket(url, protocols);
2543
2713
  socket.addEventListener("open", () => finish(true), { once: true });
2544
2714
  socket.addEventListener("error", () => finish(false), { once: true });
2545
2715
  socket.addEventListener("close", () => finish(false), { once: true });
@@ -2548,10 +2718,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2548
2718
  }
2549
2719
  });
2550
2720
  }
2551
- async function waitForAppServerHealth(url, timeoutMs) {
2721
+ async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
2552
2722
  const deadline = Date.now() + timeoutMs;
2553
2723
  while (Date.now() < deadline) {
2554
- if (await checkAppServerHealth(url)) {
2724
+ if (await checkAppServerHealth(
2725
+ url,
2726
+ APP_SERVER_HEALTH_TIMEOUT_MS,
2727
+ gatewayToken
2728
+ )) {
2555
2729
  return true;
2556
2730
  }
2557
2731
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -2564,7 +2738,7 @@ async function terminateProcess(pid, platform) {
2564
2738
  }
2565
2739
  try {
2566
2740
  if (platform === "win32") {
2567
- execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2741
+ execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2568
2742
  } else {
2569
2743
  process.kill(pid, "SIGTERM");
2570
2744
  await delay(2e3);
@@ -2816,8 +2990,9 @@ Or start it manually:
2816
2990
  throw new Error("Tap auth gateway token is missing after startup.");
2817
2991
  }
2818
2992
  const gatewayHealthy = await waitForAppServerHealth(
2819
- buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
2820
- APP_SERVER_GATEWAY_START_TIMEOUT_MS
2993
+ effectiveUrl,
2994
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS,
2995
+ gatewayToken
2821
2996
  );
2822
2997
  if (!gatewayHealthy) {
2823
2998
  await terminateProcess(pid, options.platform);
@@ -2853,7 +3028,11 @@ function logFilePath(stateDir, instanceId) {
2853
3028
  function runtimeHeartbeatFilePath(runtimeStateDir) {
2854
3029
  return path13.join(runtimeStateDir, "heartbeat.json");
2855
3030
  }
2856
- function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
3031
+ function runtimeThreadStateFilePath(runtimeStateDir) {
3032
+ return path13.join(runtimeStateDir, "thread.json");
3033
+ }
3034
+ function loadRuntimeBridgeHeartbeat(bridgeState) {
3035
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
2857
3036
  if (!runtimeStateDir) {
2858
3037
  return null;
2859
3038
  }
@@ -2862,13 +3041,35 @@ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
2862
3041
  return null;
2863
3042
  }
2864
3043
  try {
2865
- const raw = fs13.readFileSync(heartbeatPath, "utf-8");
2866
- const parsed = JSON.parse(raw);
2867
- return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
3044
+ return JSON.parse(
3045
+ fs13.readFileSync(heartbeatPath, "utf-8")
3046
+ );
2868
3047
  } catch {
2869
3048
  return null;
2870
3049
  }
2871
3050
  }
3051
+ function loadRuntimeBridgeThreadState(bridgeState) {
3052
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
3053
+ if (!runtimeStateDir) {
3054
+ return null;
3055
+ }
3056
+ const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
3057
+ if (!fs13.existsSync(threadPath)) {
3058
+ return null;
3059
+ }
3060
+ try {
3061
+ const parsed = JSON.parse(
3062
+ fs13.readFileSync(threadPath, "utf-8")
3063
+ );
3064
+ return parsed.threadId ? parsed : null;
3065
+ } catch {
3066
+ return null;
3067
+ }
3068
+ }
3069
+ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
3070
+ const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
3071
+ return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
3072
+ }
2872
3073
  function resolveHeartbeatTimestamp(state) {
2873
3074
  return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
2874
3075
  }
@@ -3038,6 +3239,7 @@ async function startBridge(options) {
3038
3239
  options.messageLookbackMinutes
3039
3240
  )
3040
3241
  } : {},
3242
+ ...process.env.TAP_COLD_START_WARMUP === "true" ? { TAP_COLD_START_WARMUP: "true" } : {},
3041
3243
  ...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
3042
3244
  ...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
3043
3245
  ...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
@@ -3169,8 +3371,49 @@ function getBridgeStatus(stateDir, instanceId) {
3169
3371
  }
3170
3372
 
3171
3373
  // src/commands/add.ts
3374
+ var ADD_HELP = `
3375
+ Usage:
3376
+ tap add <claude|codex|gemini> [options]
3377
+
3378
+ Description:
3379
+ Install a runtime instance and configure it to use tap.
3380
+
3381
+ Options:
3382
+ --name <name> Instance name (default: runtime name)
3383
+ --port <port> Port for app-server bridge
3384
+ --agent-name <name> Agent display name for bridge identification
3385
+ --force Re-install even if already configured
3386
+ --headless Enable headless reviewer mode (requires --name)
3387
+ --role <role> Headless role: reviewer, validator, long-running
3388
+ --help, -h Show help
3389
+
3390
+ Examples:
3391
+ npx @hua-labs/tap add claude
3392
+ npx @hua-labs/tap add codex --name reviewer --port 4501 --headless --role reviewer
3393
+ `.trim();
3394
+ function normalizeAgentName(value) {
3395
+ if (typeof value !== "string") {
3396
+ return null;
3397
+ }
3398
+ const trimmed = value.trim();
3399
+ return trimmed ? trimmed : null;
3400
+ }
3401
+ function resolveAgentName2(options) {
3402
+ return normalizeAgentName(options.explicit) ?? normalizeAgentName(options.stored) ?? normalizeAgentName(options.env) ?? normalizeAgentName(options.fallback) ?? null;
3403
+ }
3172
3404
  async function addCommand(args) {
3173
3405
  const { positional, flags } = parseArgs(args);
3406
+ if (flags["help"] === true || flags["h"] === true) {
3407
+ log(ADD_HELP);
3408
+ return {
3409
+ ok: true,
3410
+ command: "add",
3411
+ code: "TAP_NO_OP",
3412
+ message: ADD_HELP,
3413
+ warnings: [],
3414
+ data: {}
3415
+ };
3416
+ }
3174
3417
  const runtimeArg = positional[0];
3175
3418
  if (!runtimeArg) {
3176
3419
  return {
@@ -3196,8 +3439,10 @@ async function addCommand(args) {
3196
3439
  const instanceName = typeof flags["name"] === "string" ? flags["name"] : void 0;
3197
3440
  const instanceId = buildInstanceId(runtime, instanceName);
3198
3441
  const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
3199
- const port = portStr ? parseInt(portStr, 10) : null;
3200
- const agentNameFlag = typeof flags["agent-name"] === "string" ? flags["agent-name"] : null;
3442
+ const port = portStr ? Number(portStr) : null;
3443
+ const agentNameFlag = normalizeAgentName(
3444
+ typeof flags["agent-name"] === "string" ? flags["agent-name"] : null
3445
+ );
3201
3446
  const force = flags["force"] === true;
3202
3447
  const headlessFlag = flags["headless"] === true;
3203
3448
  const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
@@ -3232,20 +3477,21 @@ async function addCommand(args) {
3232
3477
  maxRounds: 5,
3233
3478
  qualitySeverityFloor: "high"
3234
3479
  } : null;
3235
- if (portStr && (port === null || isNaN(port))) {
3480
+ if (portStr && (port === null || isNaN(port) || port < 1 || port > 65535)) {
3236
3481
  return {
3237
3482
  ok: false,
3238
3483
  command: "add",
3239
3484
  runtime,
3240
3485
  instanceId,
3241
3486
  code: "TAP_INVALID_ARGUMENT",
3242
- message: `Invalid port: ${portStr}`,
3487
+ message: `Invalid port: ${portStr}. Must be between 1 and 65535.`,
3243
3488
  warnings: [],
3244
3489
  data: {}
3245
3490
  };
3246
3491
  }
3247
3492
  const repoRoot = findRepoRoot();
3248
3493
  const state = loadState(repoRoot);
3494
+ const adapter = getAdapter(runtime);
3249
3495
  if (!state) {
3250
3496
  return {
3251
3497
  ok: false,
@@ -3258,7 +3504,39 @@ async function addCommand(args) {
3258
3504
  data: {}
3259
3505
  };
3260
3506
  }
3261
- if (state.instances[instanceId]?.installed && !force) {
3507
+ const existingInstance = state.instances[instanceId];
3508
+ const mode = adapter.bridgeMode();
3509
+ const envAgentName = normalizeAgentName(
3510
+ process.env.TAP_AGENT_NAME ?? process.env.CODEX_TAP_AGENT_NAME
3511
+ );
3512
+ const defaultAgentName = mode === "app-server" ? instanceId : null;
3513
+ const resolvedAgentName = resolveAgentName2({
3514
+ explicit: agentNameFlag,
3515
+ env: envAgentName,
3516
+ stored: existingInstance?.agentName ?? null,
3517
+ fallback: defaultAgentName
3518
+ });
3519
+ if (existingInstance?.installed && !force) {
3520
+ if (resolvedAgentName !== existingInstance.agentName) {
3521
+ const updatedState = updateInstanceState(state, instanceId, {
3522
+ ...existingInstance,
3523
+ agentName: resolvedAgentName
3524
+ });
3525
+ saveState(repoRoot, updatedState);
3526
+ return {
3527
+ ok: true,
3528
+ command: "add",
3529
+ runtime,
3530
+ instanceId,
3531
+ code: "TAP_ADD_OK",
3532
+ message: resolvedAgentName === null ? `${instanceId} updated` : `${instanceId} agent name updated to "${resolvedAgentName}".`,
3533
+ warnings: [],
3534
+ data: {
3535
+ updatedFields: ["agentName"],
3536
+ agentName: resolvedAgentName
3537
+ }
3538
+ };
3539
+ }
3262
3540
  return {
3263
3541
  ok: true,
3264
3542
  command: "add",
@@ -3288,14 +3566,12 @@ async function addCommand(args) {
3288
3566
  logHeader(`@hua-labs/tap add ${instanceId}`);
3289
3567
  if (instanceName) log(`Instance name: ${instanceName}`);
3290
3568
  if (port !== null) log(`Port: ${port}`);
3291
- const existingAgentName = state.instances[instanceId]?.agentName ?? null;
3292
- const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
3569
+ if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3293
3570
  const ctx = {
3294
3571
  ...createAdapterContext(state.commsDir, repoRoot),
3295
3572
  instanceId,
3296
- agentName: effectiveAgentName
3573
+ agentName: resolvedAgentName ?? void 0
3297
3574
  };
3298
- const adapter = getAdapter(runtime);
3299
3575
  const warnings = [];
3300
3576
  log("Probing runtime...");
3301
3577
  const probe = await adapter.probe(ctx);
@@ -3375,7 +3651,6 @@ async function addCommand(args) {
3375
3651
  );
3376
3652
  }
3377
3653
  let bridge = null;
3378
- const mode = adapter.bridgeMode();
3379
3654
  if (mode === "app-server") {
3380
3655
  const bridgeScript = adapter.resolveBridgeScript?.(ctx);
3381
3656
  if (!bridgeScript) {
@@ -3383,36 +3658,34 @@ async function addCommand(args) {
3383
3658
  warnings.push("Bridge script not found. Run bridge manually.");
3384
3659
  } else {
3385
3660
  const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3386
- {
3387
- log(`Starting bridge: ${bridgeScript}`);
3388
- try {
3389
- bridge = await startBridge({
3390
- instanceId,
3391
- runtime,
3392
- stateDir: ctx.stateDir,
3393
- commsDir: ctx.commsDir,
3394
- bridgeScript,
3395
- platform: ctx.platform,
3396
- agentName: agentNameFlag ?? void 0,
3397
- runtimeCommand: resolvedCfg.runtimeCommand,
3398
- appServerUrl: resolvedCfg.appServerUrl,
3399
- repoRoot,
3400
- port: port ?? void 0,
3401
- headless
3402
- });
3403
- logSuccess(`Bridge started (PID: ${bridge.pid})`);
3404
- } catch (err) {
3405
- const msg = err instanceof Error ? err.message : String(err);
3406
- logWarn(`Bridge not started: ${msg}`);
3407
- warnings.push(`Bridge not started: ${msg}`);
3408
- }
3661
+ log(`Starting bridge: ${bridgeScript}`);
3662
+ try {
3663
+ bridge = await startBridge({
3664
+ instanceId,
3665
+ runtime,
3666
+ stateDir: ctx.stateDir,
3667
+ commsDir: ctx.commsDir,
3668
+ bridgeScript,
3669
+ platform: ctx.platform,
3670
+ agentName: resolvedAgentName ?? void 0,
3671
+ runtimeCommand: resolvedCfg.runtimeCommand,
3672
+ appServerUrl: resolvedCfg.appServerUrl,
3673
+ repoRoot,
3674
+ port: port ?? void 0,
3675
+ headless
3676
+ });
3677
+ logSuccess(`Bridge started (PID: ${bridge.pid})`);
3678
+ } catch (err) {
3679
+ const msg = err instanceof Error ? err.message : String(err);
3680
+ logWarn(`Bridge not started: ${msg}`);
3681
+ warnings.push(`Bridge not started: ${msg}`);
3409
3682
  }
3410
3683
  }
3411
3684
  }
3412
3685
  const instanceState = {
3413
3686
  instanceId,
3414
3687
  runtime,
3415
- agentName: agentNameFlag ?? existingAgentName,
3688
+ agentName: resolvedAgentName,
3416
3689
  port,
3417
3690
  installed: true,
3418
3691
  configPath: probe.configPath ?? "",
@@ -3424,7 +3697,7 @@ async function addCommand(args) {
3424
3697
  lastVerifiedAt: verify.ok ? (/* @__PURE__ */ new Date()).toISOString() : null,
3425
3698
  bridge,
3426
3699
  headless,
3427
- warnings: [...result.warnings, ...verify.warnings]
3700
+ warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
3428
3701
  };
3429
3702
  const newState = updateInstanceState(state, instanceId, instanceState);
3430
3703
  saveState(repoRoot, newState);
@@ -3432,6 +3705,13 @@ async function addCommand(args) {
3432
3705
  if (result.restartRequired) {
3433
3706
  logWarn(`Restart ${runtime} to pick up the new configuration.`);
3434
3707
  }
3708
+ if (runtime === "claude") {
3709
+ log("");
3710
+ log("For real-time notifications:");
3711
+ log(" claude --dangerously-load-development-channels server:tap-comms");
3712
+ log("Or polling mode (tools still work):");
3713
+ log(" claude");
3714
+ }
3435
3715
  logHeader("Done!");
3436
3716
  return {
3437
3717
  ok: true,
@@ -3451,6 +3731,16 @@ async function addCommand(args) {
3451
3731
  }
3452
3732
 
3453
3733
  // src/commands/status.ts
3734
+ var STATUS_HELP = `
3735
+ Usage:
3736
+ tap status
3737
+
3738
+ Description:
3739
+ Show all installed instances, their bridge status, and configuration info.
3740
+
3741
+ Examples:
3742
+ npx @hua-labs/tap status
3743
+ `.trim();
3454
3744
  function resolveStatus(inst, stateDir) {
3455
3745
  if (!inst.installed) return "not installed";
3456
3746
  switch (inst.bridgeMode) {
@@ -3477,7 +3767,18 @@ function instanceStatusLine(inst, status) {
3477
3767
  const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
3478
3768
  return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
3479
3769
  }
3480
- async function statusCommand(_args) {
3770
+ async function statusCommand(args) {
3771
+ if (args.includes("--help") || args.includes("-h")) {
3772
+ log(STATUS_HELP);
3773
+ return {
3774
+ ok: true,
3775
+ command: "status",
3776
+ code: "TAP_NO_OP",
3777
+ message: STATUS_HELP,
3778
+ warnings: [],
3779
+ data: {}
3780
+ };
3781
+ }
3481
3782
  const repoRoot = findRepoRoot();
3482
3783
  const state = loadState(repoRoot);
3483
3784
  if (!state) {
@@ -3706,7 +4007,32 @@ function cleanEmptyParents(obj, keyPath) {
3706
4007
  }
3707
4008
 
3708
4009
  // src/commands/remove.ts
4010
+ var REMOVE_HELP = `
4011
+ Usage:
4012
+ tap remove <instance>
4013
+
4014
+ Description:
4015
+ Remove a registered instance, stop its bridge, and rollback config changes.
4016
+
4017
+ Arguments:
4018
+ <instance> Instance ID or runtime name (e.g. claude, codex-reviewer)
4019
+
4020
+ Examples:
4021
+ npx @hua-labs/tap remove claude
4022
+ npx @hua-labs/tap remove codex-reviewer
4023
+ `.trim();
3709
4024
  async function removeCommand(args) {
4025
+ if (args.includes("--help") || args.includes("-h")) {
4026
+ log(REMOVE_HELP);
4027
+ return {
4028
+ ok: true,
4029
+ command: "remove",
4030
+ code: "TAP_NO_OP",
4031
+ message: REMOVE_HELP,
4032
+ warnings: [],
4033
+ data: {}
4034
+ };
4035
+ }
3710
4036
  const identifier = args.find((a) => !a.startsWith("-"));
3711
4037
  if (!identifier) {
3712
4038
  return {
@@ -3742,8 +4068,8 @@ async function removeCommand(args) {
3742
4068
  };
3743
4069
  }
3744
4070
  const instanceId = resolved.instanceId;
3745
- const instance = state.instances[instanceId];
3746
- if (!instance?.installed) {
4071
+ const instance2 = state.instances[instanceId];
4072
+ if (!instance2?.installed) {
3747
4073
  return {
3748
4074
  ok: true,
3749
4075
  command: "remove",
@@ -3755,7 +4081,7 @@ async function removeCommand(args) {
3755
4081
  };
3756
4082
  }
3757
4083
  logHeader(`@hua-labs/tap remove ${instanceId}`);
3758
- if (instance.bridge) {
4084
+ if (instance2.bridge) {
3759
4085
  const ctx = createAdapterContext(state.commsDir, repoRoot);
3760
4086
  const stopped = await stopBridge({
3761
4087
  instanceId,
@@ -3768,7 +4094,7 @@ async function removeCommand(args) {
3768
4094
  log(`No running bridge for ${instanceId}`);
3769
4095
  }
3770
4096
  }
3771
- const result = await rollbackRuntime(instanceId, instance);
4097
+ const result = await rollbackRuntime(instanceId, instance2);
3772
4098
  if (result.success) {
3773
4099
  logSuccess(`Rolled back ${result.restoredCount} artifact(s)`);
3774
4100
  for (const f of result.restoredFiles) logSuccess(`Restored: ${f}`);
@@ -3780,7 +4106,7 @@ async function removeCommand(args) {
3780
4106
  ok: true,
3781
4107
  command: "remove",
3782
4108
  instanceId,
3783
- runtime: instance.runtime,
4109
+ runtime: instance2.runtime,
3784
4110
  code: "TAP_REMOVE_OK",
3785
4111
  message: `${instanceId} removed successfully`,
3786
4112
  warnings: [],
@@ -3795,7 +4121,7 @@ async function removeCommand(args) {
3795
4121
  ok: false,
3796
4122
  command: "remove",
3797
4123
  instanceId,
3798
- runtime: instance.runtime,
4124
+ runtime: instance2.runtime,
3799
4125
  code: "TAP_ROLLBACK_FAILED",
3800
4126
  message: "Rollback had errors. State preserved for retry.",
3801
4127
  warnings: result.errors,
@@ -3812,7 +4138,7 @@ function formatAge(seconds) {
3812
4138
  }
3813
4139
  var BRIDGE_HELP = `
3814
4140
  Usage:
3815
- tap-comms bridge <subcommand> [instance] [options]
4141
+ tap bridge <subcommand> [instance] [options]
3816
4142
 
3817
4143
  Subcommands:
3818
4144
  start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
@@ -3824,7 +4150,7 @@ Subcommands:
3824
4150
 
3825
4151
  Options:
3826
4152
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
3827
- Saved to state \u2014 only needed on first start
4153
+ Overrides the stored name from 'tap add' when needed
3828
4154
  --all Start all registered app-server instances
3829
4155
  --busy-mode <steer|wait> How to handle active turns (default: steer)
3830
4156
  --poll-seconds <n> Inbox poll interval (default: 5)
@@ -3860,16 +4186,31 @@ function redactProtectedUrl(url) {
3860
4186
  try {
3861
4187
  const parsed = new URL(url);
3862
4188
  if (parsed.searchParams.has("tap_token")) {
3863
- parsed.searchParams.set("tap_token", "***");
4189
+ parsed.searchParams.delete("tap_token");
3864
4190
  }
3865
4191
  return parsed.toString().replace(/\/$/, "");
3866
4192
  } catch {
3867
- return url.replace(/tap_token=[^&]+/g, "tap_token=***");
4193
+ return url.replace(/[?&]tap_token=[^&]+/g, "");
3868
4194
  }
3869
4195
  }
3870
4196
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
3871
4197
  return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
3872
4198
  }
4199
+ function formatThreadSummary(threadId, cwd) {
4200
+ if (!threadId) {
4201
+ return "-";
4202
+ }
4203
+ return cwd ? `${threadId} (${cwd})` : threadId;
4204
+ }
4205
+ function normalizeComparablePath(value) {
4206
+ return path14.resolve(value).replace(/\\/g, "/").toLowerCase();
4207
+ }
4208
+ function sameOptionalPath(left, right) {
4209
+ if (!left || !right) {
4210
+ return left === right;
4211
+ }
4212
+ return normalizeComparablePath(left) === normalizeComparablePath(right);
4213
+ }
3873
4214
  function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
3874
4215
  const shared = [];
3875
4216
  for (const [id, inst] of Object.entries(state.instances)) {
@@ -3947,37 +4288,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3947
4288
  };
3948
4289
  }
3949
4290
  const instanceId = resolved.instanceId;
3950
- let instance = state.instances[instanceId];
3951
- if (!instance?.installed) {
4291
+ let instance2 = state.instances[instanceId];
4292
+ if (!instance2?.installed) {
3952
4293
  return {
3953
4294
  ok: false,
3954
4295
  command: "bridge",
3955
4296
  instanceId,
3956
- runtime: instance?.runtime,
4297
+ runtime: instance2?.runtime,
3957
4298
  code: "TAP_INSTANCE_NOT_FOUND",
3958
- message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
4299
+ message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance2?.runtime ?? identifier}`,
3959
4300
  warnings: [],
3960
4301
  data: {}
3961
4302
  };
3962
4303
  }
3963
- const adapter = getAdapter(instance.runtime);
4304
+ const adapter = getAdapter(instance2.runtime);
3964
4305
  const mode = adapter.bridgeMode();
3965
4306
  if (mode !== "app-server") {
3966
4307
  return {
3967
4308
  ok: true,
3968
4309
  command: "bridge",
3969
4310
  instanceId,
3970
- runtime: instance.runtime,
4311
+ runtime: instance2.runtime,
3971
4312
  code: "TAP_NO_OP",
3972
4313
  message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3973
4314
  warnings: [],
3974
4315
  data: { bridgeMode: mode }
3975
4316
  };
3976
4317
  }
3977
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3978
- if (agentName && agentName !== instance.agentName) {
3979
- instance = { ...instance, agentName };
3980
- const updatedState = updateInstanceState(state, instanceId, instance);
4318
+ const resolvedAgentName = agentName ?? instance2.agentName ?? void 0;
4319
+ if (agentName && agentName !== instance2.agentName) {
4320
+ instance2 = { ...instance2, agentName };
4321
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3981
4322
  saveState(repoRoot, updatedState);
3982
4323
  state = updatedState;
3983
4324
  }
@@ -3988,7 +4329,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3988
4329
  ok: false,
3989
4330
  command: "bridge",
3990
4331
  instanceId,
3991
- runtime: instance.runtime,
4332
+ runtime: instance2.runtime,
3992
4333
  code: "TAP_BRIDGE_SCRIPT_MISSING",
3993
4334
  message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3994
4335
  warnings: [],
@@ -3997,8 +4338,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3997
4338
  }
3998
4339
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3999
4340
  const runtimeCommand = resolvedConfig.runtimeCommand;
4000
- const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
4001
- let effectivePort = instance.port;
4341
+ const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
4342
+ let effectivePort = instance2.port;
4002
4343
  if (effectivePort == null && manageAppServer) {
4003
4344
  effectivePort = await findNextAvailableAppServerPort(
4004
4345
  state,
@@ -4006,8 +4347,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4006
4347
  4501,
4007
4348
  instanceId
4008
4349
  );
4009
- instance = { ...instance, port: effectivePort };
4010
- const updatedState = updateInstanceState(state, instanceId, instance);
4350
+ instance2 = { ...instance2, port: effectivePort };
4351
+ const updatedState = updateInstanceState(state, instanceId, instance2);
4011
4352
  saveState(repoRoot, updatedState);
4012
4353
  state = updatedState;
4013
4354
  }
@@ -4023,19 +4364,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4023
4364
  if (effectivePort != null) log(`Port: ${effectivePort}`);
4024
4365
  if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
4025
4366
  const noAuth = flags["no-auth"] === true;
4026
- if (!manageAppServer && instance.runtime === "codex") {
4367
+ if (!manageAppServer && instance2.runtime === "codex") {
4027
4368
  log("Auto server: disabled (--no-server)");
4028
4369
  }
4029
4370
  if (noAuth && manageAppServer) {
4030
4371
  log("Auth gateway: disabled (--no-auth)");
4031
4372
  }
4032
- const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
4373
+ const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
4033
4374
  if (willBeHeadless) {
4034
- const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
4375
+ const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance2.headless?.role ?? "reviewer";
4035
4376
  log(`Headless: ${role}`);
4036
4377
  }
4037
4378
  try {
4038
- if (!manageAppServer && instance.runtime === "codex") {
4379
+ if (!manageAppServer && instance2.runtime === "codex") {
4039
4380
  log("Checking app-server health...");
4040
4381
  const healthy = await checkAppServerHealth(appServerUrl);
4041
4382
  if (healthy) {
@@ -4046,7 +4387,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4046
4387
  ok: false,
4047
4388
  command: "bridge",
4048
4389
  instanceId,
4049
- runtime: instance.runtime,
4390
+ runtime: instance2.runtime,
4050
4391
  code: "TAP_BRIDGE_START_FAILED",
4051
4392
  message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
4052
4393
  warnings: [],
@@ -4060,7 +4401,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4060
4401
  ok: false,
4061
4402
  command: "bridge",
4062
4403
  instanceId,
4063
- runtime: instance.runtime,
4404
+ runtime: instance2.runtime,
4064
4405
  code: "TAP_INVALID_ARGUMENT",
4065
4406
  message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
4066
4407
  warnings: [],
@@ -4068,9 +4409,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4068
4409
  };
4069
4410
  }
4070
4411
  const busyMode = busyModeRaw;
4071
- const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
4072
- const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
4073
- const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
4412
+ const pollSecondsRaw = typeof flags["poll-seconds"] === "string" ? flags["poll-seconds"] : void 0;
4413
+ const reconnectSecondsRaw = typeof flags["reconnect-seconds"] === "string" ? flags["reconnect-seconds"] : void 0;
4414
+ const lookbackRaw = typeof flags["message-lookback-minutes"] === "string" ? flags["message-lookback-minutes"] : void 0;
4415
+ let pollSeconds;
4416
+ let reconnectSeconds;
4417
+ let messageLookbackMinutes;
4418
+ try {
4419
+ pollSeconds = parseIntFlag(pollSecondsRaw, "--poll-seconds", 1, 3600);
4420
+ reconnectSeconds = parseIntFlag(
4421
+ reconnectSecondsRaw,
4422
+ "--reconnect-seconds",
4423
+ 1,
4424
+ 3600
4425
+ );
4426
+ messageLookbackMinutes = parseIntFlag(
4427
+ lookbackRaw,
4428
+ "--message-lookback-minutes",
4429
+ 1,
4430
+ 10080
4431
+ );
4432
+ } catch (err) {
4433
+ return {
4434
+ ok: false,
4435
+ command: "bridge",
4436
+ instanceId,
4437
+ runtime: instance2.runtime,
4438
+ code: "TAP_INVALID_ARGUMENT",
4439
+ message: err instanceof Error ? err.message : String(err),
4440
+ warnings: [],
4441
+ data: {}
4442
+ };
4443
+ }
4074
4444
  const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
4075
4445
  const ephemeral = flags["ephemeral"] === true;
4076
4446
  const processExistingMessages = flags["process-existing-messages"] === true;
@@ -4082,7 +4452,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4082
4452
  ok: false,
4083
4453
  command: "bridge",
4084
4454
  instanceId,
4085
- runtime: instance.runtime,
4455
+ runtime: instance2.runtime,
4086
4456
  code: "TAP_INVALID_ARGUMENT",
4087
4457
  message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
4088
4458
  warnings: [],
@@ -4094,10 +4464,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4094
4464
  role: roleArg ?? "reviewer",
4095
4465
  maxRounds: 5,
4096
4466
  qualitySeverityFloor: "high"
4097
- } : instance.headless;
4467
+ } : instance2.headless;
4098
4468
  const bridge = await startBridge({
4099
4469
  instanceId,
4100
- runtime: instance.runtime,
4470
+ runtime: instance2.runtime,
4101
4471
  stateDir: ctx.stateDir,
4102
4472
  commsDir: ctx.commsDir,
4103
4473
  bridgeScript,
@@ -4138,14 +4508,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4138
4508
  log(`TUI connect: ${bridge.appServer.url}`);
4139
4509
  }
4140
4510
  }
4141
- const updated = { ...instance, bridge, manageAppServer, noAuth };
4511
+ const updated = { ...instance2, bridge, manageAppServer, noAuth };
4142
4512
  const newState = updateInstanceState(state, instanceId, updated);
4143
4513
  saveState(repoRoot, newState);
4144
4514
  return {
4145
4515
  ok: true,
4146
4516
  command: "bridge",
4147
4517
  instanceId,
4148
- runtime: instance.runtime,
4518
+ runtime: instance2.runtime,
4149
4519
  code: "TAP_BRIDGE_START_OK",
4150
4520
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
4151
4521
  warnings: [],
@@ -4158,7 +4528,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4158
4528
  ok: false,
4159
4529
  command: "bridge",
4160
4530
  instanceId,
4161
- runtime: instance.runtime,
4531
+ runtime: instance2.runtime,
4162
4532
  code: "TAP_BRIDGE_START_FAILED",
4163
4533
  message: msg,
4164
4534
  warnings: [],
@@ -4260,11 +4630,11 @@ async function bridgeStopOne(identifier) {
4260
4630
  }
4261
4631
  const instanceId = resolved.instanceId;
4262
4632
  const ctx = createAdapterContext(state.commsDir, repoRoot);
4263
- const instance = state.instances[instanceId];
4633
+ const instance2 = state.instances[instanceId];
4264
4634
  const bridgeState = loadCurrentBridgeState(
4265
4635
  ctx.stateDir,
4266
4636
  instanceId,
4267
- instance?.bridge
4637
+ instance2?.bridge
4268
4638
  );
4269
4639
  const appServer = bridgeState?.appServer ?? null;
4270
4640
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
@@ -4312,8 +4682,8 @@ async function bridgeStopOne(identifier) {
4312
4682
  }
4313
4683
  }
4314
4684
  }
4315
- if (instance) {
4316
- const updated = { ...instance, bridge: null };
4685
+ if (instance2) {
4686
+ const updated = { ...instance2, bridge: null };
4317
4687
  const newState = updateInstanceState(state, instanceId, updated);
4318
4688
  saveState(repoRoot, newState);
4319
4689
  }
@@ -4385,9 +4755,9 @@ async function bridgeStopAll() {
4385
4755
  logSuccess(`Stopped bridge for ${instanceId}`);
4386
4756
  stopped.push(instanceId);
4387
4757
  }
4388
- const instance = state.instances[instanceId];
4389
- if (instance?.bridge) {
4390
- state.instances[instanceId] = { ...instance, bridge: null };
4758
+ const instance2 = state.instances[instanceId];
4759
+ if (instance2?.bridge) {
4760
+ state.instances[instanceId] = { ...instance2, bridge: null };
4391
4761
  stateChanged = true;
4392
4762
  }
4393
4763
  }
@@ -4453,12 +4823,18 @@ function bridgeStatusAll() {
4453
4823
  pid: null,
4454
4824
  port: inst.port,
4455
4825
  lastHeartbeat: null,
4826
+ threadId: null,
4827
+ threadCwd: null,
4828
+ savedThreadId: null,
4829
+ savedThreadCwd: null,
4456
4830
  appServer: null
4457
4831
  };
4458
4832
  continue;
4459
4833
  }
4460
4834
  const status = getBridgeStatus(stateDir, instanceId);
4461
4835
  const bridgeState = loadBridgeState(stateDir, instanceId);
4836
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
4837
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
4462
4838
  const age = getHeartbeatAge(stateDir, instanceId);
4463
4839
  const pid = bridgeState?.pid ?? null;
4464
4840
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
@@ -4480,12 +4856,26 @@ function bridgeStatusAll() {
4480
4856
  );
4481
4857
  }
4482
4858
  }
4859
+ if (runtimeHeartbeat?.threadId) {
4860
+ log(
4861
+ ` Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
4862
+ );
4863
+ }
4864
+ if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
4865
+ log(
4866
+ ` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
4867
+ );
4868
+ }
4483
4869
  bridges[instanceId] = {
4484
4870
  status,
4485
4871
  runtime: inst.runtime,
4486
4872
  pid,
4487
4873
  port: inst.port,
4488
4874
  lastHeartbeat: heartbeat,
4875
+ threadId: runtimeHeartbeat?.threadId ?? null,
4876
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
4877
+ savedThreadId: savedThread?.threadId ?? null,
4878
+ savedThreadCwd: savedThread?.cwd ?? null,
4489
4879
  appServer: bridgeState?.appServer ?? null
4490
4880
  };
4491
4881
  }
@@ -4561,6 +4951,10 @@ function bridgeStatusOne(identifier) {
4561
4951
  pid: null,
4562
4952
  port: inst.port,
4563
4953
  lastHeartbeat: null,
4954
+ threadId: null,
4955
+ threadCwd: null,
4956
+ savedThreadId: null,
4957
+ savedThreadCwd: null,
4564
4958
  appServer: null
4565
4959
  }
4566
4960
  };
@@ -4569,6 +4963,8 @@ function bridgeStatusOne(identifier) {
4569
4963
  const stateDir = resolvedCfg2.stateDir;
4570
4964
  const status = getBridgeStatus(stateDir, instanceId);
4571
4965
  const bridgeState = loadBridgeState(stateDir, instanceId);
4966
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
4967
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
4572
4968
  const age = getHeartbeatAge(stateDir, instanceId);
4573
4969
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
4574
4970
  log(`Status: ${status}`);
@@ -4577,6 +4973,16 @@ function bridgeStatusOne(identifier) {
4577
4973
  log(
4578
4974
  `Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
4579
4975
  );
4976
+ if (runtimeHeartbeat?.threadId) {
4977
+ log(
4978
+ `Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
4979
+ );
4980
+ }
4981
+ if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
4982
+ log(
4983
+ `Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
4984
+ );
4985
+ }
4580
4986
  log(
4581
4987
  `Log: ${path14.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
4582
4988
  );
@@ -4625,6 +5031,10 @@ function bridgeStatusOne(identifier) {
4625
5031
  pid: bridgeState?.pid ?? null,
4626
5032
  port: inst.port,
4627
5033
  lastHeartbeat: heartbeat,
5034
+ threadId: runtimeHeartbeat?.threadId ?? null,
5035
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
5036
+ savedThreadId: savedThread?.threadId ?? null,
5037
+ savedThreadCwd: savedThread?.cwd ?? null,
4628
5038
  appServer: bridgeState?.appServer ?? null
4629
5039
  }
4630
5040
  };
@@ -4683,8 +5093,22 @@ async function bridgeRestart(identifier, flags) {
4683
5093
  };
4684
5094
  }
4685
5095
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
4686
- const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
4687
- const drainTimeout = parseInt(drainStr, 10) || 30;
5096
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : void 0;
5097
+ let drainTimeout;
5098
+ try {
5099
+ drainTimeout = parseIntFlag(drainStr, "--drain-timeout", 1, 300) ?? 30;
5100
+ } catch (err) {
5101
+ return {
5102
+ ok: false,
5103
+ command: "bridge",
5104
+ instanceId,
5105
+ runtime: instance.runtime,
5106
+ code: "TAP_INVALID_ARGUMENT",
5107
+ message: err instanceof Error ? err.message : String(err),
5108
+ warnings: [],
5109
+ data: {}
5110
+ };
5111
+ }
4688
5112
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
4689
5113
  log(`Drain timeout: ${drainTimeout}s`);
4690
5114
  try {
@@ -4831,7 +5255,7 @@ async function bridgeCommand(args) {
4831
5255
  // src/engine/dashboard.ts
4832
5256
  import * as fs15 from "fs";
4833
5257
  import * as path15 from "path";
4834
- import { execSync as execSync5 } from "child_process";
5258
+ import { execSync as execSync4 } from "child_process";
4835
5259
  function collectAgents(commsDir) {
4836
5260
  const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
4837
5261
  if (!fs15.existsSync(heartbeatsPath)) return [];
@@ -4910,7 +5334,7 @@ function collectBridges(repoRoot) {
4910
5334
  }
4911
5335
  function collectPRs() {
4912
5336
  try {
4913
- const output = execSync5(
5337
+ const output = execSync4(
4914
5338
  "gh pr list --state all --limit 10 --json number,title,author,state,url",
4915
5339
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
4916
5340
  );
@@ -4980,7 +5404,7 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
4980
5404
  // src/commands/up.ts
4981
5405
  var UP_HELP = `
4982
5406
  Usage:
4983
- tap-comms up [bridge-start options]
5407
+ tap up [bridge-start options]
4984
5408
 
4985
5409
  Description:
4986
5410
  Start all registered app-server bridge daemons with one command.
@@ -5004,7 +5428,18 @@ async function upCommand(args) {
5004
5428
  };
5005
5429
  }
5006
5430
  const repoRoot = findRepoRoot();
5007
- const result = await bridgeCommand(["start", "--all", ...args]);
5431
+ const previousColdStartWarmup = process.env.TAP_COLD_START_WARMUP;
5432
+ process.env.TAP_COLD_START_WARMUP = "true";
5433
+ let result;
5434
+ try {
5435
+ result = await bridgeCommand(["start", "--all", ...args]);
5436
+ } finally {
5437
+ if (previousColdStartWarmup === void 0) {
5438
+ delete process.env.TAP_COLD_START_WARMUP;
5439
+ } else {
5440
+ process.env.TAP_COLD_START_WARMUP = previousColdStartWarmup;
5441
+ }
5442
+ }
5008
5443
  const snapshot = collectDashboardSnapshot(repoRoot);
5009
5444
  const activeBridges = snapshot.bridges.filter(
5010
5445
  (bridge) => bridge.status === "running"
@@ -5035,7 +5470,7 @@ async function upCommand(args) {
5035
5470
  // src/commands/down.ts
5036
5471
  var DOWN_HELP = `
5037
5472
  Usage:
5038
- tap-comms down
5473
+ tap down
5039
5474
 
5040
5475
  Description:
5041
5476
  Stop all running bridge daemons and managed app-servers.
@@ -5084,7 +5519,34 @@ async function downCommand(args) {
5084
5519
  // src/commands/serve.ts
5085
5520
  import * as path16 from "path";
5086
5521
  import { spawn as spawn2 } from "child_process";
5522
+ var SERVE_HELP = `
5523
+ Usage:
5524
+ tap serve [options]
5525
+
5526
+ Description:
5527
+ Start the tap MCP server over stdio. This command takes over the
5528
+ process \u2014 it is intended to be launched by an MCP host (e.g. Claude Code).
5529
+
5530
+ Options:
5531
+ --comms-dir <path> Override comms directory (also reads TAP_COMMS_DIR env)
5532
+ --help, -h Show help
5533
+
5534
+ Examples:
5535
+ npx @hua-labs/tap serve
5536
+ npx @hua-labs/tap serve --comms-dir /shared/comms
5537
+ `.trim();
5087
5538
  async function serveCommand(args) {
5539
+ if (args.includes("--help") || args.includes("-h")) {
5540
+ log(SERVE_HELP);
5541
+ return {
5542
+ ok: true,
5543
+ command: "serve",
5544
+ code: "TAP_NO_OP",
5545
+ message: SERVE_HELP,
5546
+ warnings: [],
5547
+ data: {}
5548
+ };
5549
+ }
5088
5550
  const repoRoot = findRepoRoot();
5089
5551
  let commsDir;
5090
5552
  const commsDirIdx = args.indexOf("--comms-dir");
@@ -5132,9 +5594,9 @@ async function serveCommand(args) {
5132
5594
  TAP_COMMS_DIR: commsDir
5133
5595
  }
5134
5596
  });
5135
- return new Promise((resolve11) => {
5597
+ return new Promise((resolve13) => {
5136
5598
  child.on("error", (err) => {
5137
- resolve11({
5599
+ resolve13({
5138
5600
  ok: false,
5139
5601
  command: "serve",
5140
5602
  code: "TAP_INTERNAL_ERROR",
@@ -5144,7 +5606,7 @@ async function serveCommand(args) {
5144
5606
  });
5145
5607
  });
5146
5608
  child.on("exit", (code) => {
5147
- resolve11({
5609
+ resolve13({
5148
5610
  ok: code === 0,
5149
5611
  command: "serve",
5150
5612
  code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
@@ -5159,10 +5621,10 @@ async function serveCommand(args) {
5159
5621
  // src/commands/init-worktree.ts
5160
5622
  import * as fs16 from "fs";
5161
5623
  import * as path17 from "path";
5162
- import { execSync as execSync6 } from "child_process";
5624
+ import { execSync as execSync5 } from "child_process";
5163
5625
  var INIT_WORKTREE_HELP = `
5164
5626
  Usage:
5165
- tap-comms init-worktree [options]
5627
+ tap init-worktree [options]
5166
5628
 
5167
5629
  Options:
5168
5630
  --path <dir> Worktree directory (required, e.g. ../hua-wt-3)
@@ -5183,7 +5645,7 @@ function warn(warnings, message) {
5183
5645
  }
5184
5646
  function run(cmd, opts) {
5185
5647
  try {
5186
- return execSync6(cmd, {
5648
+ return execSync5(cmd, {
5187
5649
  cwd: opts?.cwd,
5188
5650
  encoding: "utf-8",
5189
5651
  stdio: ["pipe", "pipe", "pipe"],
@@ -5200,7 +5662,7 @@ function toAbsolute(p) {
5200
5662
  }
5201
5663
  function probeBun(candidate) {
5202
5664
  try {
5203
- const out = execSync6(`"${candidate}" --version`, {
5665
+ const out = execSync5(`"${candidate}" --version`, {
5204
5666
  encoding: "utf-8",
5205
5667
  stdio: ["pipe", "pipe", "pipe"],
5206
5668
  timeout: 5e3
@@ -5214,7 +5676,7 @@ function findBun() {
5214
5676
  const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
5215
5677
  for (const name of candidates) {
5216
5678
  try {
5217
- const out = execSync6(
5679
+ const out = execSync5(
5218
5680
  process.platform === "win32" ? `where ${name}` : `which ${name}`,
5219
5681
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
5220
5682
  ).trim();
@@ -5345,7 +5807,7 @@ function step4GenerateMcpJson(opts, warnings) {
5345
5807
  );
5346
5808
  const mcpConfig = {
5347
5809
  mcpServers: {
5348
- "tap-comms": {
5810
+ tap: {
5349
5811
  command: bunAbs,
5350
5812
  args: [channelEntry],
5351
5813
  cwd: wtAbs,
@@ -5445,7 +5907,7 @@ async function initWorktreeCommand(args) {
5445
5907
  ok: true,
5446
5908
  command: "init-worktree",
5447
5909
  code: "TAP_NO_OP",
5448
- message: "init-worktree help",
5910
+ message: INIT_WORKTREE_HELP,
5449
5911
  warnings: [],
5450
5912
  data: {}
5451
5913
  };
@@ -5607,8 +6069,38 @@ function renderSnapshot(snapshot) {
5607
6069
  }
5608
6070
  }
5609
6071
  }
6072
+ var DASHBOARD_HELP = `
6073
+ Usage:
6074
+ tap dashboard [options]
6075
+
6076
+ Description:
6077
+ Display a unified ops dashboard: agents, bridges, PRs, and warnings.
6078
+
6079
+ Options:
6080
+ --json Output snapshot as JSON
6081
+ --watch Refresh dashboard on an interval
6082
+ --interval <seconds> Refresh interval in seconds (default: 5, min: 2)
6083
+ --comms-dir <path> Override comms directory
6084
+ --help, -h Show help
6085
+
6086
+ Examples:
6087
+ npx @hua-labs/tap dashboard
6088
+ npx @hua-labs/tap dashboard --watch --interval 10
6089
+ npx @hua-labs/tap dashboard --json
6090
+ `.trim();
5610
6091
  async function dashboardCommand(args) {
5611
6092
  const { flags } = parseArgs(args);
6093
+ if (flags["help"] === true || flags["h"] === true) {
6094
+ log(DASHBOARD_HELP);
6095
+ return {
6096
+ ok: true,
6097
+ command: "dashboard",
6098
+ code: "TAP_NO_OP",
6099
+ message: DASHBOARD_HELP,
6100
+ warnings: [],
6101
+ data: {}
6102
+ };
6103
+ }
5612
6104
  const jsonMode = flags["json"] === true;
5613
6105
  const watchMode = flags["watch"] === true;
5614
6106
  const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
@@ -5668,14 +6160,150 @@ import {
5668
6160
  mkdirSync as mkdirSync10,
5669
6161
  readdirSync as readdirSync4,
5670
6162
  readFileSync as readFileSync14,
6163
+ renameSync as renameSync10,
5671
6164
  statSync as statSync2,
5672
- unlinkSync as unlinkSync3
6165
+ unlinkSync as unlinkSync3,
6166
+ writeFileSync as writeFileSync12
5673
6167
  } from "fs";
5674
- import { execSync as execSync7 } from "child_process";
5675
- import { join as join17 } from "path";
6168
+ import { homedir as homedir3 } from "os";
6169
+ import { spawnSync as spawnSync4 } from "child_process";
6170
+ import { dirname as dirname11, join as join17, resolve as resolve12 } from "path";
5676
6171
  var PASS = "pass";
5677
6172
  var WARN = "warn";
5678
6173
  var FAIL = "fail";
6174
+ var CODEX_ENV_DRIFT_KEYS = [
6175
+ "TAP_COMMS_DIR",
6176
+ "TAP_STATE_DIR",
6177
+ "TAP_REPO_ROOT"
6178
+ ];
6179
+ function normalizeComparablePath2(value) {
6180
+ return resolve12(value).replace(/\\/g, "/").toLowerCase();
6181
+ }
6182
+ function samePath(left, right) {
6183
+ return normalizeComparablePath2(left) === normalizeComparablePath2(right);
6184
+ }
6185
+ function looksLikePathToken(value) {
6186
+ return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("/") || value.startsWith("\\") || value.startsWith(".") || value.includes("/") || value.includes("\\");
6187
+ }
6188
+ function sameCommandToken(left, right) {
6189
+ return looksLikePathToken(left) || looksLikePathToken(right) ? samePath(left, right) : left === right;
6190
+ }
6191
+ function sameStringArray(left, right) {
6192
+ return left.length === right.length && left.every((value, index) => sameCommandToken(value, right[index] ?? ""));
6193
+ }
6194
+ function appendWarningMessage(message, extra) {
6195
+ return message.includes(extra) ? message : `${message}; ${extra}`;
6196
+ }
6197
+ function findCodexConfigPath3() {
6198
+ return join17(homedir3(), ".codex", "config.toml");
6199
+ }
6200
+ function canonicalizeTrustPath3(targetPath) {
6201
+ let resolved = resolve12(targetPath).replace(/\//g, "\\");
6202
+ const driveRoot = /^[A-Za-z]:\\$/;
6203
+ if (!driveRoot.test(resolved)) {
6204
+ resolved = resolved.replace(/\\+$/g, "");
6205
+ }
6206
+ return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
6207
+ }
6208
+ function trustSelector2(targetPath) {
6209
+ return `projects.'${canonicalizeTrustPath3(targetPath)}'`;
6210
+ }
6211
+ function writeTomlAtomically(filePath, content) {
6212
+ const dir = dirname11(filePath);
6213
+ mkdirSync10(dir, { recursive: true });
6214
+ const tmp = `${filePath}.tmp.${process.pid}`;
6215
+ writeFileSync12(tmp, content, "utf-8");
6216
+ renameSync10(tmp, filePath);
6217
+ }
6218
+ function hasInstalledCodexInstance(state) {
6219
+ return !!state ? Object.values(state.instances).some(
6220
+ (instance2) => instance2.runtime === "codex" && instance2.installed
6221
+ ) : false;
6222
+ }
6223
+ function getCodexTrustTargets(repoRoot) {
6224
+ return [...new Set([repoRoot, process.cwd()].map((value) => resolve12(value)))];
6225
+ }
6226
+ function buildCodexDoctorSpec(repoRoot, commsDir) {
6227
+ const state = loadState(repoRoot);
6228
+ if (!hasInstalledCodexInstance(state)) {
6229
+ return null;
6230
+ }
6231
+ const ctx = createAdapterContext(commsDir, repoRoot);
6232
+ const managed = buildManagedMcpServerSpec(ctx);
6233
+ return {
6234
+ configPath: findCodexConfigPath3(),
6235
+ trustTargets: getCodexTrustTargets(repoRoot),
6236
+ managed
6237
+ };
6238
+ }
6239
+ function repairCodexConfig(repoRoot, commsDir) {
6240
+ const spec = buildCodexDoctorSpec(repoRoot, commsDir);
6241
+ if (!spec) {
6242
+ throw new Error("No installed Codex instance found in tap state.");
6243
+ }
6244
+ if (!spec.managed.command || spec.managed.issues.length > 0) {
6245
+ throw new Error(
6246
+ spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
6247
+ );
6248
+ }
6249
+ const existingContent = existsSync16(spec.configPath) ? readFileSync14(spec.configPath, "utf-8") : "";
6250
+ const existingTapEnvTable = extractTomlTable(existingContent, "mcp_servers.tap.env");
6251
+ const existingLegacyEnvTable = extractTomlTable(
6252
+ existingContent,
6253
+ "mcp_servers.tap-comms.env"
6254
+ );
6255
+ const preservedEnv = parseTomlAssignments(
6256
+ existingTapEnvTable ?? existingLegacyEnvTable ?? ""
6257
+ );
6258
+ const repairedEnv = {
6259
+ ...preservedEnv,
6260
+ ...Object.fromEntries(
6261
+ CODEX_ENV_DRIFT_KEYS.map((key) => [key, spec.managed.env[key]])
6262
+ )
6263
+ };
6264
+ let nextContent = existingContent;
6265
+ if (extractTomlTable(nextContent, "mcp_servers.tap-comms.env")) {
6266
+ nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms.env");
6267
+ }
6268
+ if (extractTomlTable(nextContent, "mcp_servers.tap-comms")) {
6269
+ nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms");
6270
+ }
6271
+ nextContent = replaceTomlTable(
6272
+ nextContent,
6273
+ "mcp_servers.tap",
6274
+ renderTomlTable(
6275
+ "mcp_servers.tap",
6276
+ {
6277
+ command: spec.managed.command,
6278
+ args: spec.managed.args
6279
+ },
6280
+ extractTomlTable(existingContent, "mcp_servers.tap")
6281
+ )
6282
+ );
6283
+ nextContent = replaceTomlTable(
6284
+ nextContent,
6285
+ "mcp_servers.tap.env",
6286
+ renderTomlTable(
6287
+ "mcp_servers.tap.env",
6288
+ repairedEnv,
6289
+ existingTapEnvTable ?? existingLegacyEnvTable
6290
+ )
6291
+ );
6292
+ for (const trustTarget of spec.trustTargets) {
6293
+ const selector = trustSelector2(trustTarget);
6294
+ nextContent = replaceTomlTable(
6295
+ nextContent,
6296
+ selector,
6297
+ renderTomlTable(
6298
+ selector,
6299
+ { trust_level: "trusted" },
6300
+ extractTomlTable(existingContent, selector)
6301
+ )
6302
+ );
6303
+ }
6304
+ writeTomlAtomically(spec.configPath, nextContent);
6305
+ return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
6306
+ }
5679
6307
  function countFiles(dir, ext = ".md") {
5680
6308
  if (!existsSync16(dir)) return 0;
5681
6309
  try {
@@ -5700,21 +6328,6 @@ function recentFileCount(dir, withinMs) {
5700
6328
  }
5701
6329
  return count;
5702
6330
  }
5703
- function loadBridgeRuntimeHeartbeat(bridgeState) {
5704
- const runtimeStateDir = bridgeState?.runtimeStateDir;
5705
- if (!runtimeStateDir) {
5706
- return null;
5707
- }
5708
- const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
5709
- if (!existsSync16(heartbeatPath)) {
5710
- return null;
5711
- }
5712
- try {
5713
- return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
5714
- } catch {
5715
- return null;
5716
- }
5717
- }
5718
6331
  function checkComms(commsDir) {
5719
6332
  const checks = [];
5720
6333
  checks.push({
@@ -5798,7 +6411,8 @@ function checkInstances(repoRoot, stateDir) {
5798
6411
  const running = isBridgeRunning(stateDir, id);
5799
6412
  const bridgeState = loadBridgeState(stateDir, id);
5800
6413
  const heartbeatAge = getHeartbeatAge(stateDir, id);
5801
- const runtimeHeartbeat = loadBridgeRuntimeHeartbeat(bridgeState);
6414
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
6415
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
5802
6416
  let status;
5803
6417
  let message;
5804
6418
  let fix;
@@ -5844,9 +6458,30 @@ function checkInstances(repoRoot, stateDir) {
5844
6458
  }
5845
6459
  const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
5846
6460
  if (lastRuntimeError) {
5847
- status = status === FAIL ? FAIL : WARN;
6461
+ status = WARN;
5848
6462
  message = `${message}; bridge last error: ${lastRuntimeError}`;
5849
6463
  }
6464
+ if (savedThread?.threadId && savedThread.cwd && !samePath(savedThread.cwd, repoRoot)) {
6465
+ status = WARN;
6466
+ message = appendWarningMessage(
6467
+ message,
6468
+ `saved thread cwd mismatch (${savedThread.cwd})`
6469
+ );
6470
+ }
6471
+ if (runtimeHeartbeat?.threadId && savedThread?.threadId && runtimeHeartbeat.threadId !== savedThread.threadId) {
6472
+ status = WARN;
6473
+ message = appendWarningMessage(
6474
+ message,
6475
+ `saved thread ${savedThread.threadId} differs from active thread ${runtimeHeartbeat.threadId}`
6476
+ );
6477
+ }
6478
+ if (runtimeHeartbeat?.threadCwd && !samePath(runtimeHeartbeat.threadCwd, repoRoot)) {
6479
+ status = WARN;
6480
+ message = appendWarningMessage(
6481
+ message,
6482
+ `active thread cwd mismatch (${runtimeHeartbeat.threadCwd})`
6483
+ );
6484
+ }
5850
6485
  checks.push({ name: `bridge: ${id}`, status, message, fix });
5851
6486
  } else {
5852
6487
  checks.push({
@@ -5919,15 +6554,25 @@ function checkMcpServer(repoRoot) {
5919
6554
  });
5920
6555
  return checks;
5921
6556
  }
5922
- const hasTapComms = config?.mcpServers?.["tap-comms"];
5923
- if (!hasTapComms) {
6557
+ const mcpServers = config?.mcpServers;
6558
+ const hasTap = mcpServers?.["tap"];
6559
+ const hasOldKey = mcpServers?.["tap-comms"];
6560
+ if (hasOldKey) {
5924
6561
  checks.push({
5925
6562
  name: "MCP config (.mcp.json)",
5926
6563
  status: WARN,
5927
- message: "tap-comms not configured"
6564
+ message: 'Legacy "tap-comms" key found. Run "tap add claude" to migrate to the new "tap" key.'
6565
+ });
6566
+ }
6567
+ if (!hasTap && !hasOldKey) {
6568
+ checks.push({
6569
+ name: "MCP config (.mcp.json)",
6570
+ status: WARN,
6571
+ message: "tap not configured"
5928
6572
  });
5929
6573
  return checks;
5930
6574
  }
6575
+ const hasTapComms = hasTap ?? hasOldKey;
5931
6576
  checks.push({
5932
6577
  name: "MCP config (.mcp.json)",
5933
6578
  status: PASS,
@@ -5938,11 +6583,12 @@ function checkMcpServer(repoRoot) {
5938
6583
  let cmdAvailable = existsSync16(cmd);
5939
6584
  if (!cmdAvailable) {
5940
6585
  try {
5941
- execSync7(`"${cmd}" --version`, {
6586
+ const result = spawnSync4(cmd, ["--version"], {
5942
6587
  stdio: "pipe",
5943
- timeout: 5e3
6588
+ timeout: 5e3,
6589
+ shell: process.platform === "win32"
5944
6590
  });
5945
- cmdAvailable = true;
6591
+ cmdAvailable = result.status === 0;
5946
6592
  } catch {
5947
6593
  }
5948
6594
  }
@@ -6001,6 +6647,88 @@ function checkMcpServer(repoRoot) {
6001
6647
  });
6002
6648
  return checks;
6003
6649
  }
6650
+ function checkCodexConfig(repoRoot, commsDir) {
6651
+ const spec = buildCodexDoctorSpec(repoRoot, commsDir);
6652
+ if (!spec) {
6653
+ return [];
6654
+ }
6655
+ const checks = [];
6656
+ const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
6657
+ if (!existsSync16(spec.configPath)) {
6658
+ checks.push({
6659
+ name: "MCP config (~/.codex/config.toml)",
6660
+ status: WARN,
6661
+ message: `${spec.configPath} not found. ${fixHint}`,
6662
+ fix: () => repairCodexConfig(repoRoot, commsDir)
6663
+ });
6664
+ return checks;
6665
+ }
6666
+ const content = readFileSync14(spec.configPath, "utf-8");
6667
+ const tapTable = extractTomlTable(content, "mcp_servers.tap");
6668
+ const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
6669
+ const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
6670
+ const legacyEnvTable = extractTomlTable(content, "mcp_servers.tap-comms.env");
6671
+ const selectedMain = parseTomlAssignments(tapTable ?? "");
6672
+ const selectedEnv = parseTomlAssignments(
6673
+ tapEnvTable ?? legacyEnvTable ?? ""
6674
+ );
6675
+ const issues = [];
6676
+ if (legacyTable || legacyEnvTable) {
6677
+ issues.push('legacy "tap-comms" key present');
6678
+ }
6679
+ if (!tapTable && !legacyTable) {
6680
+ issues.push("tap MCP table missing");
6681
+ }
6682
+ if (!tapEnvTable && !legacyEnvTable) {
6683
+ issues.push("tap MCP env table missing");
6684
+ }
6685
+ if (tapTable && spec.managed.command) {
6686
+ const actualCommand = selectedMain.command;
6687
+ if (typeof actualCommand !== "string") {
6688
+ issues.push("tap MCP command missing");
6689
+ } else if (!sameCommandToken(actualCommand, spec.managed.command)) {
6690
+ issues.push(`tap MCP command drift (${actualCommand})`);
6691
+ }
6692
+ const actualArgs = selectedMain.args;
6693
+ if (!Array.isArray(actualArgs)) {
6694
+ issues.push("tap MCP args missing");
6695
+ } else if (!sameStringArray(actualArgs, spec.managed.args)) {
6696
+ issues.push(`tap MCP args drift (${JSON.stringify(actualArgs)})`);
6697
+ }
6698
+ }
6699
+ for (const key of CODEX_ENV_DRIFT_KEYS) {
6700
+ const expected = spec.managed.env[key];
6701
+ const actual = selectedEnv[key];
6702
+ if (typeof actual !== "string") {
6703
+ issues.push(`${key} missing`);
6704
+ continue;
6705
+ }
6706
+ if (!samePath(actual, expected)) {
6707
+ issues.push(`${key} drift (${actual})`);
6708
+ }
6709
+ }
6710
+ for (const trustTarget of spec.trustTargets) {
6711
+ const trustTable = extractTomlTable(content, trustSelector2(trustTarget));
6712
+ if (!trustTable || !trustTable.includes('trust_level = "trusted"')) {
6713
+ issues.push(`missing trust for ${trustTarget}`);
6714
+ }
6715
+ }
6716
+ if (issues.length === 0) {
6717
+ checks.push({
6718
+ name: "MCP config (~/.codex/config.toml)",
6719
+ status: PASS,
6720
+ message: spec.configPath
6721
+ });
6722
+ return checks;
6723
+ }
6724
+ checks.push({
6725
+ name: "MCP config (~/.codex/config.toml)",
6726
+ status: WARN,
6727
+ message: `${issues.join("; ")}. ${fixHint}`,
6728
+ fix: () => repairCodexConfig(repoRoot, commsDir)
6729
+ });
6730
+ return checks;
6731
+ }
6004
6732
  function checkBridgeTurnHealth(repoRoot) {
6005
6733
  const checks = [];
6006
6734
  const tmpDir = join17(repoRoot, ".tmp");
@@ -6115,7 +6843,35 @@ function renderCheck(check, fixMode) {
6115
6843
  const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
6116
6844
  return ` ${icon} ${check.name}${msg}`;
6117
6845
  }
6846
+ var DOCTOR_HELP = `
6847
+ Usage:
6848
+ tap doctor [options]
6849
+
6850
+ Description:
6851
+ Diagnose tap infrastructure health: comms directory, instances, bridges,
6852
+ message lifecycle, and MCP server configuration.
6853
+
6854
+ Options:
6855
+ --fix Auto-repair detected issues where possible
6856
+ --comms-dir <path> Override comms directory
6857
+ --help, -h Show help
6858
+
6859
+ Examples:
6860
+ npx @hua-labs/tap doctor
6861
+ npx @hua-labs/tap doctor --fix
6862
+ `.trim();
6118
6863
  async function doctorCommand(args) {
6864
+ if (args.includes("--help") || args.includes("-h")) {
6865
+ log(DOCTOR_HELP);
6866
+ return {
6867
+ ok: true,
6868
+ command: "doctor",
6869
+ code: "TAP_NO_OP",
6870
+ message: DOCTOR_HELP,
6871
+ warnings: [],
6872
+ data: {}
6873
+ };
6874
+ }
6119
6875
  const repoRoot = findRepoRoot();
6120
6876
  const overrides = {};
6121
6877
  let fixMode = false;
@@ -6137,6 +6893,7 @@ async function doctorCommand(args) {
6137
6893
  checks.push(...checkInstances(repoRoot, config.stateDir));
6138
6894
  checks.push(...checkMessageLifecycle(commsDir));
6139
6895
  checks.push(...checkMcpServer(repoRoot));
6896
+ checks.push(...checkCodexConfig(repoRoot, commsDir));
6140
6897
  checks.push(...checkBridgeTurnHealth(repoRoot));
6141
6898
  return checks;
6142
6899
  }
@@ -6228,12 +6985,12 @@ async function doctorCommand(args) {
6228
6985
  }
6229
6986
 
6230
6987
  // src/commands/comms.ts
6231
- import { execSync as execSync8 } from "child_process";
6988
+ import { execSync as execSync6, spawnSync as spawnSync5 } from "child_process";
6232
6989
  import * as fs17 from "fs";
6233
6990
  import * as path18 from "path";
6234
6991
  var COMMS_HELP = `
6235
6992
  Usage:
6236
- tap-comms comms <subcommand>
6993
+ tap comms <subcommand>
6237
6994
 
6238
6995
  Subcommands:
6239
6996
  pull Pull latest changes from comms remote repo
@@ -6260,7 +7017,7 @@ function commsPull(commsDir) {
6260
7017
  };
6261
7018
  }
6262
7019
  try {
6263
- const output = execSync8("git pull --rebase", {
7020
+ const output = execSync6("git pull --rebase", {
6264
7021
  cwd: commsDir,
6265
7022
  encoding: "utf-8",
6266
7023
  stdio: "pipe"
@@ -6302,8 +7059,8 @@ function commsPush(commsDir) {
6302
7059
  };
6303
7060
  }
6304
7061
  try {
6305
- execSync8("git add -A", { cwd: commsDir, stdio: "pipe" });
6306
- const status = execSync8("git status --porcelain", {
7062
+ execSync6("git add -A", { cwd: commsDir, stdio: "pipe" });
7063
+ const status = execSync6("git status --porcelain", {
6307
7064
  cwd: commsDir,
6308
7065
  encoding: "utf-8",
6309
7066
  stdio: "pipe"
@@ -6320,11 +7077,23 @@ function commsPush(commsDir) {
6320
7077
  };
6321
7078
  }
6322
7079
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6323
- execSync8(`git commit -m "chore(comms): sync ${timestamp}"`, {
6324
- cwd: commsDir,
6325
- stdio: "pipe"
6326
- });
6327
- execSync8("git push", { cwd: commsDir, stdio: "pipe" });
7080
+ const commitResult = spawnSync5(
7081
+ "git",
7082
+ ["commit", "-m", `chore(comms): sync ${timestamp}`],
7083
+ { cwd: commsDir, stdio: "pipe", encoding: "utf-8" }
7084
+ );
7085
+ if (commitResult.status !== 0) {
7086
+ const msg = commitResult.stderr || `git commit exited with code ${commitResult.status}`;
7087
+ return {
7088
+ ok: false,
7089
+ command: "comms",
7090
+ code: "TAP_COMMS_PUSH_FAILED",
7091
+ message: `Commit failed: ${msg}`,
7092
+ warnings: [],
7093
+ data: { commsDir }
7094
+ };
7095
+ }
7096
+ execSync6("git push", { cwd: commsDir, stdio: "pipe" });
6328
7097
  logSuccess("Comms push complete");
6329
7098
  return {
6330
7099
  ok: true,
@@ -6390,7 +7159,12 @@ function emitResult(result, jsonMode) {
6390
7159
  } else {
6391
7160
  logError(result.message);
6392
7161
  }
7162
+ const emittedWarnings = /* @__PURE__ */ new Set();
6393
7163
  for (const w of result.warnings) {
7164
+ if (emittedWarnings.has(w) || wasWarningLogged(w)) {
7165
+ continue;
7166
+ }
7167
+ emittedWarnings.add(w);
6394
7168
  logWarn(w);
6395
7169
  }
6396
7170
  }
@@ -6403,16 +7177,61 @@ function extractJsonFlag(args) {
6403
7177
  return { jsonMode, cleanArgs };
6404
7178
  }
6405
7179
 
7180
+ // src/cli-suggest.ts
7181
+ var COMMANDS = [
7182
+ "init",
7183
+ "init-worktree",
7184
+ "add",
7185
+ "remove",
7186
+ "status",
7187
+ "bridge",
7188
+ "up",
7189
+ "down",
7190
+ "comms",
7191
+ "dashboard",
7192
+ "doctor",
7193
+ "serve",
7194
+ "version"
7195
+ ];
7196
+ function suggestCommand(input) {
7197
+ let best = null;
7198
+ let bestDist = Infinity;
7199
+ for (const cmd of COMMANDS) {
7200
+ const d = levenshtein(input.toLowerCase(), cmd);
7201
+ if (d < bestDist && d <= Math.max(2, Math.floor(cmd.length / 2))) {
7202
+ bestDist = d;
7203
+ best = cmd;
7204
+ }
7205
+ }
7206
+ return best;
7207
+ }
7208
+ function levenshtein(a, b) {
7209
+ const m = a.length;
7210
+ const n = b.length;
7211
+ const dp = Array.from(
7212
+ { length: m + 1 },
7213
+ () => Array.from({ length: n + 1 }).fill(0)
7214
+ );
7215
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
7216
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
7217
+ for (let i = 1; i <= m; i++) {
7218
+ for (let j = 1; j <= n; j++) {
7219
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
7220
+ }
7221
+ }
7222
+ return dp[m][n];
7223
+ }
7224
+
6406
7225
  // src/cli.ts
6407
7226
  var HELP = `
6408
7227
  @hua-labs/tap \u2014 Cross-model AI agent communication setup
6409
7228
 
6410
7229
  Usage:
6411
- tap-comms <command> [options]
7230
+ tap <command> [options]
6412
7231
 
6413
7232
  Commands:
6414
7233
  init Initialize comms directory and state
6415
- init-worktree Set up a new git worktree with tap-comms
7234
+ init-worktree Set up a new git worktree with tap
6416
7235
  add <runtime> Add a runtime instance (claude, codex, gemini)
6417
7236
  remove <instance> Remove an instance and rollback config
6418
7237
  status Show installed instances and bridge status
@@ -6422,7 +7241,7 @@ Commands:
6422
7241
  comms <pull|push> Sync comms directory with remote repo
6423
7242
  dashboard Show unified ops dashboard
6424
7243
  doctor Diagnose tap infrastructure health
6425
- serve Start tap-comms MCP server (stdio)
7244
+ serve Start tap MCP server (stdio)
6426
7245
  version Show version
6427
7246
 
6428
7247
  Options:
@@ -6459,6 +7278,7 @@ function normalizeCommandName(command) {
6459
7278
  async function main() {
6460
7279
  const rawArgs = process.argv.slice(2);
6461
7280
  const { jsonMode, cleanArgs } = extractJsonFlag(rawArgs);
7281
+ resetLoggedWarnings();
6462
7282
  setJsonMode(jsonMode);
6463
7283
  const command = cleanArgs[0];
6464
7284
  if (!command || command === "--help" || command === "-h") {
@@ -6516,21 +7336,27 @@ async function main() {
6516
7336
  break;
6517
7337
  case "serve": {
6518
7338
  const serveResult = await serveCommand(commandArgs);
6519
- if (!serveResult.ok) {
7339
+ if (!serveResult.ok || serveResult.code === "TAP_NO_OP") {
6520
7340
  emitResult(serveResult, jsonMode);
6521
7341
  }
6522
7342
  process.exit(exitCode(serveResult));
6523
7343
  break;
6524
7344
  }
6525
- default:
7345
+ default: {
7346
+ const suggestion = suggestCommand(command);
7347
+ const hint = suggestion ? `
7348
+
7349
+ Did you mean: tap ${suggestion}?` : "\n\nRun tap --help for a list of commands.";
6526
7350
  result = {
6527
7351
  ok: false,
6528
7352
  command: "unknown",
6529
7353
  code: "TAP_INVALID_ARGUMENT",
6530
- message: `Unknown command: ${command}`,
7354
+ message: `Unknown command: ${command}${hint}`,
6531
7355
  warnings: [],
6532
- data: { requestedCommand: command }
7356
+ data: { requestedCommand: command, suggestion }
6533
7357
  };
7358
+ break;
7359
+ }
6534
7360
  }
6535
7361
  } catch (err) {
6536
7362
  const message = err instanceof Error ? err.message : String(err);