@hua-labs/tap 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,29 +1,33 @@
1
- // src/commands/init.ts
2
- import * as fs6 from "fs";
3
- import * as path6 from "path";
4
- import { spawnSync } from "child_process";
5
-
6
- // src/state.ts
7
- import * as fs3 from "fs";
8
- import * as path3 from "path";
9
- import * as crypto from "crypto";
10
-
11
- // src/config/resolve.ts
12
- import * as fs2 from "fs";
13
- import * as path2 from "path";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __esm = (fn, res) => function __init() {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ };
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
14
21
 
15
22
  // src/utils.ts
16
23
  import * as fs from "fs";
17
24
  import * as path from "path";
18
- var VALID_RUNTIMES = ["claude", "codex", "gemini"];
19
25
  function isValidRuntime(name) {
20
26
  return VALID_RUNTIMES.includes(name);
21
27
  }
22
28
  function detectPlatform() {
23
29
  return process.platform;
24
30
  }
25
- var _noGitWarned = false;
26
- var _loggedWarnings = /* @__PURE__ */ new Set();
27
31
  function _setNoGitWarned() {
28
32
  _noGitWarned = true;
29
33
  }
@@ -97,7 +101,6 @@ function parseArgs(args) {
97
101
  }
98
102
  return { positional, flags };
99
103
  }
100
- var _jsonMode = false;
101
104
  function setJsonMode(enabled) {
102
105
  _jsonMode = enabled;
103
106
  }
@@ -156,7 +159,17 @@ function resolveInstanceId(identifier, state) {
156
159
  message: `Instance not found: ${identifier}`
157
160
  };
158
161
  }
162
+ function validateInstanceName(name) {
163
+ if (/[/\\]/.test(name) || name.includes("..")) {
164
+ throw new Error(
165
+ `Invalid instance name "${name}": must not contain path separators or ".." sequences`
166
+ );
167
+ }
168
+ }
159
169
  function buildInstanceId(runtime, name) {
170
+ if (name) {
171
+ validateInstanceName(name);
172
+ }
160
173
  return name ? `${runtime}-${name}` : runtime;
161
174
  }
162
175
  function findPortConflict(state, port, excludeInstanceId) {
@@ -165,13 +178,21 @@ function findPortConflict(state, port, excludeInstanceId) {
165
178
  }
166
179
  return null;
167
180
  }
181
+ var VALID_RUNTIMES, _noGitWarned, _loggedWarnings, _jsonMode;
182
+ var init_utils = __esm({
183
+ "src/utils.ts"() {
184
+ "use strict";
185
+ init_config();
186
+ VALID_RUNTIMES = ["claude", "codex", "gemini"];
187
+ _noGitWarned = false;
188
+ _loggedWarnings = /* @__PURE__ */ new Set();
189
+ _jsonMode = false;
190
+ }
191
+ });
168
192
 
169
193
  // src/config/resolve.ts
170
- var SHARED_CONFIG_FILE = "tap-config.json";
171
- var LOCAL_CONFIG_FILE = "tap-config.local.json";
172
- var LEGACY_CONFIG_FILE = ".tap-config";
173
- var DEFAULT_RUNTIME_COMMAND = "node";
174
- var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
194
+ import * as fs2 from "fs";
195
+ import * as path2 from "path";
175
196
  function findRepoRoot2(startDir = process.cwd()) {
176
197
  let dir = path2.resolve(startDir);
177
198
  while (true) {
@@ -344,19 +365,430 @@ function normalizeTapPath(input) {
344
365
  }
345
366
  return trimmed;
346
367
  }
368
+ var SHARED_CONFIG_FILE, LOCAL_CONFIG_FILE, LEGACY_CONFIG_FILE, DEFAULT_RUNTIME_COMMAND, DEFAULT_APP_SERVER_URL;
369
+ var init_resolve = __esm({
370
+ "src/config/resolve.ts"() {
371
+ "use strict";
372
+ init_utils();
373
+ SHARED_CONFIG_FILE = "tap-config.json";
374
+ LOCAL_CONFIG_FILE = "tap-config.local.json";
375
+ LEGACY_CONFIG_FILE = ".tap-config";
376
+ DEFAULT_RUNTIME_COMMAND = "node";
377
+ DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
378
+ }
379
+ });
380
+
381
+ // src/permissions/presets.ts
382
+ function createPermissionFromRole(role) {
383
+ const preset = ROLE_PRESETS[role];
384
+ return {
385
+ ...preset,
386
+ allowedTools: [...preset.allowedTools],
387
+ deniedTools: [...preset.deniedTools],
388
+ allowedPaths: [...preset.allowedPaths]
389
+ };
390
+ }
391
+ var ROLE_PRESETS;
392
+ var init_presets = __esm({
393
+ "src/permissions/presets.ts"() {
394
+ "use strict";
395
+ ROLE_PRESETS = {
396
+ tower: {
397
+ role: "tower",
398
+ mode: "full-access",
399
+ allowedTools: ["*"],
400
+ deniedTools: [],
401
+ allowedPaths: ["**"],
402
+ escalateTo: null
403
+ },
404
+ implementer: {
405
+ role: "implementer",
406
+ mode: "workspace-write",
407
+ allowedTools: [
408
+ "Read",
409
+ "Edit",
410
+ "Write",
411
+ "Bash",
412
+ "Grep",
413
+ "Glob",
414
+ "mcp__tap__*"
415
+ ],
416
+ deniedTools: ["Bash(git push --force:*)", "Bash(git reset --hard:*)"],
417
+ allowedPaths: ["packages/**", "apps/**", "docs/**"],
418
+ escalateTo: "tower"
419
+ },
420
+ reviewer: {
421
+ role: "reviewer",
422
+ mode: "readonly",
423
+ allowedTools: [
424
+ "Read",
425
+ "Grep",
426
+ "Glob",
427
+ "Bash(grep:*)",
428
+ "Bash(git diff:*)",
429
+ "mcp__tap__*"
430
+ ],
431
+ deniedTools: ["Edit", "Write", "Bash(rm:*)"],
432
+ allowedPaths: ["hua-comms/reviews/**"],
433
+ escalateTo: "tower"
434
+ },
435
+ custom: {
436
+ role: "custom",
437
+ mode: "prompt",
438
+ allowedTools: [],
439
+ deniedTools: [],
440
+ allowedPaths: [],
441
+ escalateTo: "tower"
442
+ }
443
+ };
444
+ }
445
+ });
446
+
447
+ // src/config/instance-config.ts
448
+ var instance_config_exports = {};
449
+ __export(instance_config_exports, {
450
+ createInstanceConfig: () => createInstanceConfig,
451
+ deleteInstanceConfig: () => deleteInstanceConfig,
452
+ listInstanceConfigs: () => listInstanceConfigs,
453
+ loadInstanceConfig: () => loadInstanceConfig,
454
+ saveInstanceConfig: () => saveInstanceConfig,
455
+ updateInstanceConfig: () => updateInstanceConfig
456
+ });
457
+ import * as fs3 from "fs";
458
+ import * as path3 from "path";
459
+ function instancesDir(stateDir) {
460
+ return path3.join(stateDir, "instances");
461
+ }
462
+ function instanceConfigPath(stateDir, instanceId) {
463
+ if (instanceId.includes("/") || instanceId.includes("\\") || instanceId.includes("..")) {
464
+ throw new Error(
465
+ `Invalid instanceId "${instanceId}": must not contain path separators or ".." sequences`
466
+ );
467
+ }
468
+ return path3.join(instancesDir(stateDir), `${instanceId}.json`);
469
+ }
470
+ function loadInstanceConfig(stateDir, instanceId) {
471
+ const filePath = instanceConfigPath(stateDir, instanceId);
472
+ if (!fs3.existsSync(filePath)) return null;
473
+ try {
474
+ const raw = fs3.readFileSync(filePath, "utf-8");
475
+ const parsed = JSON.parse(raw);
476
+ if (!parsed.permission) {
477
+ parsed.permission = createPermissionFromRole("custom");
478
+ }
479
+ return parsed;
480
+ } catch {
481
+ return null;
482
+ }
483
+ }
484
+ function saveInstanceConfig(stateDir, config) {
485
+ const dir = instancesDir(stateDir);
486
+ fs3.mkdirSync(dir, { recursive: true });
487
+ const filePath = instanceConfigPath(stateDir, config.instanceId);
488
+ const tmp = `${filePath}.tmp.${process.pid}`;
489
+ fs3.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
490
+ fs3.renameSync(tmp, filePath);
491
+ return filePath;
492
+ }
493
+ function listInstanceConfigs(stateDir) {
494
+ const dir = instancesDir(stateDir);
495
+ if (!fs3.existsSync(dir)) return [];
496
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
497
+ const configs = [];
498
+ for (const file of files) {
499
+ try {
500
+ const raw = fs3.readFileSync(path3.join(dir, file), "utf-8");
501
+ configs.push(JSON.parse(raw));
502
+ } catch {
503
+ }
504
+ }
505
+ return configs;
506
+ }
507
+ function deleteInstanceConfig(stateDir, instanceId) {
508
+ const filePath = instanceConfigPath(stateDir, instanceId);
509
+ if (!fs3.existsSync(filePath)) return false;
510
+ fs3.unlinkSync(filePath);
511
+ return true;
512
+ }
513
+ function createInstanceConfig(opts) {
514
+ const parts = opts.instanceId.split("-");
515
+ if (parts.length > 1) {
516
+ validateInstanceName(parts.slice(1).join("-"));
517
+ }
518
+ const now = (/* @__PURE__ */ new Date()).toISOString();
519
+ const config = {
520
+ schemaVersion: INSTANCE_CONFIG_SCHEMA_VERSION,
521
+ instanceId: opts.instanceId,
522
+ runtime: opts.runtime,
523
+ agentName: opts.agentName,
524
+ agentId: opts.agentId,
525
+ port: opts.port,
526
+ appServerUrl: opts.appServerUrl,
527
+ permission: createPermissionFromRole(opts.role ?? "custom"),
528
+ // Top-level overrides consumed by resolveTrackedConfig
529
+ commsDir: opts.commsDir,
530
+ stateDir: opts.stateDir,
531
+ mcpEnv: {
532
+ TAP_COMMS_DIR: opts.commsDir,
533
+ TAP_STATE_DIR: opts.stateDir,
534
+ TAP_REPO_ROOT: opts.repoRoot,
535
+ TAP_AGENT_NAME: opts.agentName ?? "<set-per-session>"
536
+ },
537
+ configHash: "",
538
+ lastSyncedToRuntime: null,
539
+ runtimeConfigHash: "",
540
+ createdAt: now,
541
+ updatedAt: now
542
+ };
543
+ config.configHash = computeInstanceConfigHash(config);
544
+ return config;
545
+ }
546
+ function updateInstanceConfig(existing, updates) {
547
+ const updated = {
548
+ ...existing,
549
+ ...updates,
550
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
551
+ };
552
+ if (updates.agentName !== void 0) {
553
+ updated.mcpEnv = {
554
+ ...updated.mcpEnv,
555
+ TAP_AGENT_NAME: updates.agentName ?? "<set-per-session>"
556
+ };
557
+ }
558
+ updated.configHash = computeInstanceConfigHash(updated);
559
+ return updated;
560
+ }
561
+ function computeInstanceConfigHash(config) {
562
+ const hashInput = {
563
+ instanceId: config.instanceId,
564
+ runtime: config.runtime,
565
+ agentName: config.agentName,
566
+ agentId: config.agentId,
567
+ port: config.port,
568
+ appServerUrl: config.appServerUrl,
569
+ mcpEnv: config.mcpEnv,
570
+ permission: config.permission
571
+ };
572
+ const serialized = JSON.stringify(hashInput, Object.keys(hashInput).sort());
573
+ let hash = 2166136261;
574
+ for (let i = 0; i < serialized.length; i++) {
575
+ hash ^= serialized.charCodeAt(i);
576
+ hash = Math.imul(hash, 16777619);
577
+ }
578
+ return (hash >>> 0).toString(16).padStart(8, "0");
579
+ }
580
+ var INSTANCE_CONFIG_SCHEMA_VERSION;
581
+ var init_instance_config = __esm({
582
+ "src/config/instance-config.ts"() {
583
+ "use strict";
584
+ init_utils();
585
+ init_presets();
586
+ INSTANCE_CONFIG_SCHEMA_VERSION = 1;
587
+ }
588
+ });
589
+
590
+ // src/config/drift-detector.ts
591
+ var drift_detector_exports = {};
592
+ __export(drift_detector_exports, {
593
+ checkAllDrift: () => checkAllDrift,
594
+ checkInstanceDrift: () => checkInstanceDrift,
595
+ computeFileHash: () => computeFileHash
596
+ });
597
+ import * as fs4 from "fs";
598
+ import * as crypto from "crypto";
599
+ function computeFileHash(filePath) {
600
+ if (!fs4.existsSync(filePath)) return "";
601
+ const content = fs4.readFileSync(filePath, "utf-8");
602
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
603
+ }
604
+ function checkInstanceDrift(stateDir, instanceId, state) {
605
+ const checks = [];
606
+ const instConfig = loadInstanceConfig(stateDir, instanceId);
607
+ const stateInstance = state?.instances[instanceId] ?? null;
608
+ if (!instConfig) {
609
+ if (stateInstance?.installed) {
610
+ if (!stateInstance.configSourceFile) {
611
+ return { instanceId, status: "ok", checks };
612
+ }
613
+ checks.push({
614
+ name: "instance config exists",
615
+ source: "instance-config",
616
+ target: "state-json",
617
+ status: "missing",
618
+ details: `Instance "${instanceId}" is in state.json but has no instance config file. Run "tap add ${instanceId} --force" to recreate.`,
619
+ autoFixable: false
620
+ // Cannot generate config from state alone
621
+ });
622
+ return { instanceId, status: "missing", checks };
623
+ }
624
+ return { instanceId, status: "ok", checks };
625
+ }
626
+ if (!stateInstance) {
627
+ checks.push({
628
+ name: "instance registered",
629
+ source: "instance-config",
630
+ target: "state-json",
631
+ status: "missing",
632
+ details: `Instance config exists for "${instanceId}" but not registered in state.json`,
633
+ autoFixable: false
634
+ });
635
+ return { instanceId, status: "orphaned", checks };
636
+ }
637
+ const fieldMismatches = [];
638
+ if (instConfig.agentName !== stateInstance.agentName) {
639
+ fieldMismatches.push(
640
+ `agentName: instance="${instConfig.agentName}" vs state="${stateInstance.agentName}"`
641
+ );
642
+ }
643
+ if (instConfig.port !== stateInstance.port) {
644
+ fieldMismatches.push(
645
+ `port: instance=${instConfig.port} vs state=${stateInstance.port}`
646
+ );
647
+ }
648
+ if (fieldMismatches.length > 0) {
649
+ checks.push({
650
+ name: "state consistency",
651
+ source: "instance-config",
652
+ target: "state-json",
653
+ status: "drifted",
654
+ details: fieldMismatches.join("; "),
655
+ autoFixable: true
656
+ });
657
+ } else {
658
+ checks.push({
659
+ name: "state consistency",
660
+ source: "instance-config",
661
+ target: "state-json",
662
+ status: "ok",
663
+ details: null,
664
+ autoFixable: false
665
+ });
666
+ }
667
+ const stateHash = stateInstance.configHash ?? "";
668
+ if (!stateHash) {
669
+ checks.push({
670
+ name: "config hash baseline",
671
+ source: "instance-config",
672
+ target: "state-json",
673
+ status: "drifted",
674
+ details: `configHash not baselined for "${instanceId}" \u2014 needs backfill`,
675
+ autoFixable: true
676
+ });
677
+ } else if (instConfig.configHash !== stateHash) {
678
+ checks.push({
679
+ name: "config hash",
680
+ source: "instance-config",
681
+ target: "state-json",
682
+ status: "drifted",
683
+ details: `instance hash="${instConfig.configHash}" vs state hash="${stateHash}"`,
684
+ autoFixable: true
685
+ });
686
+ } else {
687
+ checks.push({
688
+ name: "config hash",
689
+ source: "instance-config",
690
+ target: "state-json",
691
+ status: "ok",
692
+ details: null,
693
+ autoFixable: false
694
+ });
695
+ }
696
+ if (stateInstance.configPath && fs4.existsSync(stateInstance.configPath)) {
697
+ const currentRuntimeHash = computeFileHash(stateInstance.configPath);
698
+ const lastSyncedHash = instConfig.runtimeConfigHash || "";
699
+ if (!lastSyncedHash) {
700
+ checks.push({
701
+ name: "runtime config baseline",
702
+ source: "instance-config",
703
+ target: "runtime-config",
704
+ status: "drifted",
705
+ details: `runtimeConfigHash not baselined for "${instanceId}" \u2014 needs backfill`,
706
+ autoFixable: true
707
+ });
708
+ } else if (currentRuntimeHash !== lastSyncedHash) {
709
+ checks.push({
710
+ name: "runtime config",
711
+ source: "instance-config",
712
+ target: "runtime-config",
713
+ status: "drifted",
714
+ details: `${stateInstance.configPath} has changed since last sync (hash: ${currentRuntimeHash.slice(0, 8)} vs synced: ${lastSyncedHash.slice(0, 8)})`,
715
+ autoFixable: true
716
+ });
717
+ } else {
718
+ checks.push({
719
+ name: "runtime config",
720
+ source: "instance-config",
721
+ target: "runtime-config",
722
+ status: "ok",
723
+ details: null,
724
+ autoFixable: false
725
+ });
726
+ }
727
+ }
728
+ const hasDrift = checks.some((c) => c.status !== "ok");
729
+ return {
730
+ instanceId,
731
+ status: hasDrift ? "drifted" : "ok",
732
+ checks
733
+ };
734
+ }
735
+ function checkAllDrift(stateDir, state) {
736
+ const results = [];
737
+ const checkedIds = /* @__PURE__ */ new Set();
738
+ if (state) {
739
+ for (const instanceId of Object.keys(state.instances)) {
740
+ checkedIds.add(instanceId);
741
+ results.push(checkInstanceDrift(stateDir, instanceId, state));
742
+ }
743
+ }
744
+ const instancesDir2 = `${stateDir}/instances`;
745
+ if (fs4.existsSync(instancesDir2)) {
746
+ for (const file of fs4.readdirSync(instancesDir2)) {
747
+ if (!file.endsWith(".json")) continue;
748
+ const id = file.replace(/\.json$/, "");
749
+ if (!checkedIds.has(id)) {
750
+ results.push(checkInstanceDrift(stateDir, id, state));
751
+ }
752
+ }
753
+ }
754
+ return results;
755
+ }
756
+ var init_drift_detector = __esm({
757
+ "src/config/drift-detector.ts"() {
758
+ "use strict";
759
+ init_instance_config();
760
+ }
761
+ });
762
+
763
+ // src/config/index.ts
764
+ var init_config = __esm({
765
+ "src/config/index.ts"() {
766
+ "use strict";
767
+ init_resolve();
768
+ }
769
+ });
770
+
771
+ // src/commands/init.ts
772
+ import * as fs8 from "fs";
773
+ import * as path7 from "path";
774
+ import { spawnSync } from "child_process";
347
775
 
348
776
  // src/state.ts
777
+ init_config();
778
+ import * as fs5 from "fs";
779
+ import * as path4 from "path";
780
+ import * as crypto2 from "crypto";
349
781
  var STATE_FILE = "state.json";
350
- var SCHEMA_VERSION = 2;
782
+ var SCHEMA_VERSION = 3;
351
783
  function getStateDir(repoRoot) {
352
784
  const { config } = resolveConfig({}, repoRoot);
353
785
  return config.stateDir;
354
786
  }
355
787
  function getStatePath(repoRoot) {
356
- return path3.join(getStateDir(repoRoot), STATE_FILE);
788
+ return path4.join(getStateDir(repoRoot), STATE_FILE);
357
789
  }
358
790
  function stateExists(repoRoot) {
359
- return fs3.existsSync(getStatePath(repoRoot));
791
+ return fs5.existsSync(getStatePath(repoRoot));
360
792
  }
361
793
  function migrateStateV1toV2(v1) {
362
794
  const instances = {};
@@ -382,25 +814,46 @@ function migrateStateV1toV2(v1) {
382
814
  instances
383
815
  };
384
816
  }
817
+ function migrateStateV2toV3(v2) {
818
+ const instances = {};
819
+ for (const [id, inst] of Object.entries(v2.instances)) {
820
+ instances[id] = {
821
+ ...inst,
822
+ configHash: inst.configHash ?? "",
823
+ configSourceFile: inst.configSourceFile ?? ""
824
+ };
825
+ }
826
+ return {
827
+ ...v2,
828
+ schemaVersion: SCHEMA_VERSION,
829
+ instances
830
+ };
831
+ }
385
832
  function loadState(repoRoot) {
386
833
  const statePath = getStatePath(repoRoot);
387
- if (!fs3.existsSync(statePath)) return null;
388
- const raw = fs3.readFileSync(statePath, "utf-8");
834
+ if (!fs5.existsSync(statePath)) return null;
835
+ const raw = fs5.readFileSync(statePath, "utf-8");
389
836
  const parsed = JSON.parse(raw);
390
837
  if (parsed.schemaVersion === 1 || parsed.runtimes) {
391
- const migrated = migrateStateV1toV2(parsed);
392
- saveState(repoRoot, migrated);
393
- return migrated;
838
+ const v2 = migrateStateV1toV2(parsed);
839
+ const v3 = migrateStateV2toV3(v2);
840
+ saveState(repoRoot, v3);
841
+ return v3;
842
+ }
843
+ if (parsed.schemaVersion === 2) {
844
+ const v3 = migrateStateV2toV3(parsed);
845
+ saveState(repoRoot, v3);
846
+ return v3;
394
847
  }
395
848
  return parsed;
396
849
  }
397
850
  function saveState(repoRoot, state) {
398
851
  const stateDir = getStateDir(repoRoot);
399
- fs3.mkdirSync(stateDir, { recursive: true });
852
+ fs5.mkdirSync(stateDir, { recursive: true });
400
853
  const statePath = getStatePath(repoRoot);
401
854
  const tmp = `${statePath}.tmp.${process.pid}`;
402
- fs3.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
403
- fs3.renameSync(tmp, statePath);
855
+ fs5.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
856
+ fs5.renameSync(tmp, statePath);
404
857
  }
405
858
  function createInitialState(commsDir, repoRoot, packageVersion) {
406
859
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -408,8 +861,8 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
408
861
  schemaVersion: SCHEMA_VERSION,
409
862
  createdAt: now,
410
863
  updatedAt: now,
411
- commsDir: path3.resolve(commsDir),
412
- repoRoot: path3.resolve(repoRoot),
864
+ commsDir: path4.resolve(commsDir),
865
+ repoRoot: path4.resolve(repoRoot),
413
866
  packageVersion,
414
867
  instances: {}
415
868
  };
@@ -438,33 +891,36 @@ function getInstalledInstances(state) {
438
891
  );
439
892
  }
440
893
  function ensureBackupDir(stateDir, instanceId) {
441
- const backupDir = path3.join(stateDir, "backups", instanceId);
442
- fs3.mkdirSync(backupDir, { recursive: true });
894
+ const backupDir = path4.join(stateDir, "backups", instanceId);
895
+ fs5.mkdirSync(backupDir, { recursive: true });
443
896
  return backupDir;
444
897
  }
445
898
  function backupFile(filePath, backupDir) {
446
- const basename3 = path3.basename(filePath);
899
+ const basename3 = path4.basename(filePath);
447
900
  const hash = fileHash(filePath);
448
- const backupPath = path3.join(backupDir, `${basename3}.${hash}.bak`);
449
- fs3.copyFileSync(filePath, backupPath);
901
+ const backupPath = path4.join(backupDir, `${basename3}.${hash}.bak`);
902
+ fs5.copyFileSync(filePath, backupPath);
450
903
  return backupPath;
451
904
  }
452
905
  function fileHash(filePath) {
453
- if (!fs3.existsSync(filePath)) return "";
454
- const content = fs3.readFileSync(filePath);
455
- return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
906
+ if (!fs5.existsSync(filePath)) return "";
907
+ const content = fs5.readFileSync(filePath);
908
+ return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
456
909
  }
457
910
 
911
+ // src/commands/init.ts
912
+ init_utils();
913
+
458
914
  // src/version.ts
459
- import * as fs4 from "fs";
460
- import * as path4 from "path";
915
+ import * as fs6 from "fs";
916
+ import * as path5 from "path";
461
917
  import { fileURLToPath } from "url";
462
918
  var FALLBACK_VERSION = "0.0.0";
463
919
  function resolvePackageVersion(metaUrl = import.meta.url) {
464
- const moduleDir = path4.dirname(fileURLToPath(metaUrl));
465
- const packageJsonPath = path4.join(moduleDir, "..", "package.json");
920
+ const moduleDir = path5.dirname(fileURLToPath(metaUrl));
921
+ const packageJsonPath = path5.join(moduleDir, "..", "package.json");
466
922
  try {
467
- const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
923
+ const parsed = JSON.parse(fs6.readFileSync(packageJsonPath, "utf-8"));
468
924
  if (typeof parsed.version === "string" && parsed.version.trim()) {
469
925
  return parsed.version;
470
926
  }
@@ -475,8 +931,9 @@ function resolvePackageVersion(metaUrl = import.meta.url) {
475
931
  var version = resolvePackageVersion();
476
932
 
477
933
  // src/permissions.ts
478
- import * as fs5 from "fs";
479
- import * as path5 from "path";
934
+ init_utils();
935
+ import * as fs7 from "fs";
936
+ import * as path6 from "path";
480
937
  import * as os from "os";
481
938
 
482
939
  // src/toml.ts
@@ -606,13 +1063,13 @@ var CLAUDE_DENY_RULES = [
606
1063
  ];
607
1064
  function applyClaudePermissions(repoRoot, mode) {
608
1065
  const warnings = [];
609
- const claudeDir = path5.join(repoRoot, ".claude");
610
- const settingsPath = path5.join(claudeDir, "settings.local.json");
611
- fs5.mkdirSync(claudeDir, { recursive: true });
1066
+ const claudeDir = path6.join(repoRoot, ".claude");
1067
+ const settingsPath = path6.join(claudeDir, "settings.local.json");
1068
+ fs7.mkdirSync(claudeDir, { recursive: true });
612
1069
  let settings = {};
613
- if (fs5.existsSync(settingsPath)) {
1070
+ if (fs7.existsSync(settingsPath)) {
614
1071
  try {
615
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1072
+ settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
616
1073
  } catch {
617
1074
  warnings.push(
618
1075
  ".claude/settings.local.json was invalid JSON. Starting fresh."
@@ -626,8 +1083,8 @@ function applyClaudePermissions(repoRoot, mode) {
626
1083
  const cleaned = existingDeny.filter((r) => !tapRuleSet.has(r));
627
1084
  settings.deny = cleaned;
628
1085
  const tmp2 = `${settingsPath}.tmp.${process.pid}`;
629
- fs5.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
630
- fs5.renameSync(tmp2, settingsPath);
1086
+ fs7.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1087
+ fs7.renameSync(tmp2, settingsPath);
631
1088
  logWarn("Claude: full mode \u2014 tap deny rules removed. Use with caution.");
632
1089
  warnings.push("Full permission mode: tap deny rules removed.");
633
1090
  return { applied: true, warnings };
@@ -635,18 +1092,18 @@ function applyClaudePermissions(repoRoot, mode) {
635
1092
  const newDeny = [.../* @__PURE__ */ new Set([...existingDeny, ...CLAUDE_DENY_RULES])];
636
1093
  settings.deny = newDeny;
637
1094
  const tmp = `${settingsPath}.tmp.${process.pid}`;
638
- fs5.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
639
- fs5.renameSync(tmp, settingsPath);
1095
+ fs7.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1096
+ fs7.renameSync(tmp, settingsPath);
640
1097
  logSuccess(
641
1098
  `Claude: ${CLAUDE_DENY_RULES.length} deny rules applied to .claude/settings.local.json`
642
1099
  );
643
1100
  return { applied: true, warnings };
644
1101
  }
645
1102
  function findCodexConfigPath() {
646
- return path5.join(os.homedir(), ".codex", "config.toml");
1103
+ return path6.join(os.homedir(), ".codex", "config.toml");
647
1104
  }
648
1105
  function canonicalizeTrustPath(targetPath) {
649
- let resolved = path5.resolve(targetPath).replace(/\//g, "\\");
1106
+ let resolved = path6.resolve(targetPath).replace(/\//g, "\\");
650
1107
  const driveRoot = /^[A-Za-z]:\\$/;
651
1108
  if (!driveRoot.test(resolved)) {
652
1109
  resolved = resolved.replace(/\\+$/g, "");
@@ -656,10 +1113,10 @@ function canonicalizeTrustPath(targetPath) {
656
1113
  function applyCodexPermissions(repoRoot, commsDir, mode) {
657
1114
  const warnings = [];
658
1115
  const configPath = findCodexConfigPath();
659
- fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
1116
+ fs7.mkdirSync(path6.dirname(configPath), { recursive: true });
660
1117
  let content = "";
661
- if (fs5.existsSync(configPath)) {
662
- content = fs5.readFileSync(configPath, "utf-8");
1118
+ if (fs7.existsSync(configPath)) {
1119
+ content = fs7.readFileSync(configPath, "utf-8");
663
1120
  }
664
1121
  const trustTargets = getCodexWritableRoots(repoRoot, commsDir);
665
1122
  if (mode === "full") {
@@ -721,8 +1178,8 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
721
1178
  );
722
1179
  }
723
1180
  const tmp = `${configPath}.tmp.${process.pid}`;
724
- fs5.writeFileSync(tmp, content, "utf-8");
725
- fs5.renameSync(tmp, configPath);
1181
+ fs7.writeFileSync(tmp, content, "utf-8");
1182
+ fs7.renameSync(tmp, configPath);
726
1183
  const modeLabel = mode === "full" ? "danger-full-access" : "workspace-write, network=full";
727
1184
  logSuccess(
728
1185
  `Codex: sandbox=${modeLabel}, ${trustTargets.length} path(s) trusted`
@@ -731,12 +1188,12 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
731
1188
  }
732
1189
  function getCodexWritableRoots(repoRoot, commsDir) {
733
1190
  const roots = [repoRoot, commsDir];
734
- const parent = path5.dirname(repoRoot);
1191
+ const parent = path6.dirname(repoRoot);
735
1192
  for (let i = 1; i <= 4; i++) {
736
- const wtPath = path5.join(parent, `hua-wt-${i}`);
737
- if (fs5.existsSync(wtPath)) roots.push(wtPath);
1193
+ const wtPath = path6.join(parent, `hua-wt-${i}`);
1194
+ if (fs7.existsSync(wtPath)) roots.push(wtPath);
738
1195
  }
739
- return [...new Set(roots.map((r) => path5.resolve(r)))];
1196
+ return [...new Set(roots.map((r) => path6.resolve(r)))];
740
1197
  }
741
1198
  function buildPermissionSummary(mode, repoRoot, commsDir) {
742
1199
  const trustedPaths = getCodexWritableRoots(repoRoot, commsDir);
@@ -756,6 +1213,7 @@ function buildPermissionSummary(mode, repoRoot, commsDir) {
756
1213
  }
757
1214
 
758
1215
  // src/commands/init.ts
1216
+ init_config();
759
1217
  var COMMS_DIRS = [
760
1218
  "inbox",
761
1219
  "reviews",
@@ -823,9 +1281,9 @@ async function initCommand(args) {
823
1281
  const commsRepoIdx = args.indexOf("--comms-repo");
824
1282
  const commsRepoUrl = commsRepoIdx !== -1 && args[commsRepoIdx + 1] ? args[commsRepoIdx + 1] : void 0;
825
1283
  if (commsRepoUrl) {
826
- if (fs6.existsSync(commsDir) && fs6.readdirSync(commsDir).length > 0) {
827
- const gitDir = path6.join(commsDir, ".git");
828
- if (fs6.existsSync(gitDir)) {
1284
+ if (fs8.existsSync(commsDir) && fs8.readdirSync(commsDir).length > 0) {
1285
+ const gitDir = path7.join(commsDir, ".git");
1286
+ if (fs8.existsSync(gitDir)) {
829
1287
  log(`Comms directory exists: ${commsDir}`);
830
1288
  logSuccess("Comms directory is already a git repo \u2014 linking only");
831
1289
  } else {
@@ -877,7 +1335,7 @@ async function initCommand(args) {
877
1335
  sharedConfig.commsRepoUrl = commsRepoUrl;
878
1336
  configChanged = true;
879
1337
  }
880
- const commsDirRelative = path6.relative(repoRoot, commsDir);
1338
+ const commsDirRelative = path7.relative(repoRoot, commsDir);
881
1339
  if (commsDirRelative && commsDirRelative !== "tap-comms") {
882
1340
  sharedConfig.commsDir = commsDirRelative;
883
1341
  configChanged = true;
@@ -889,13 +1347,13 @@ async function initCommand(args) {
889
1347
  }
890
1348
  log(`Comms directory: ${commsDir}`);
891
1349
  for (const dir of COMMS_DIRS) {
892
- const dirPath = path6.join(commsDir, dir);
893
- fs6.mkdirSync(dirPath, { recursive: true });
1350
+ const dirPath = path7.join(commsDir, dir);
1351
+ fs8.mkdirSync(dirPath, { recursive: true });
894
1352
  logSuccess(`Created ${dir}/`);
895
1353
  }
896
- const gitignorePath = path6.join(commsDir, ".gitignore");
897
- if (!fs6.existsSync(gitignorePath)) {
898
- fs6.writeFileSync(
1354
+ const gitignorePath = path7.join(commsDir, ".gitignore");
1355
+ if (!fs8.existsSync(gitignorePath)) {
1356
+ fs8.writeFileSync(
899
1357
  gitignorePath,
900
1358
  ["tap.db", ".lock", "*.tmp.*", ".DS_Store"].join("\n") + "\n",
901
1359
  "utf-8"
@@ -904,12 +1362,12 @@ async function initCommand(args) {
904
1362
  }
905
1363
  const { config } = resolveConfig({}, repoRoot);
906
1364
  const stateDir = config.stateDir;
907
- fs6.mkdirSync(path6.join(stateDir, "pids"), { recursive: true });
908
- fs6.mkdirSync(path6.join(stateDir, "logs"), { recursive: true });
909
- fs6.mkdirSync(path6.join(stateDir, "backups"), { recursive: true });
910
- const stateDirRel = path6.relative(repoRoot, stateDir);
1365
+ fs8.mkdirSync(path7.join(stateDir, "pids"), { recursive: true });
1366
+ fs8.mkdirSync(path7.join(stateDir, "logs"), { recursive: true });
1367
+ fs8.mkdirSync(path7.join(stateDir, "backups"), { recursive: true });
1368
+ const stateDirRel = path7.relative(repoRoot, stateDir);
911
1369
  logSuccess(`Created ${stateDirRel}/ state directory`);
912
- const repoGitignore = path6.join(repoRoot, ".gitignore");
1370
+ const repoGitignore = path7.join(repoRoot, ".gitignore");
913
1371
  const gitignoreEntries = [
914
1372
  { entry: stateDirRel.replace(/\\/g, "/") + "/", label: "tap-comms state" },
915
1373
  {
@@ -917,11 +1375,11 @@ async function initCommand(args) {
917
1375
  label: "tap-comms local config (machine-specific)"
918
1376
  }
919
1377
  ];
920
- if (fs6.existsSync(repoGitignore)) {
921
- const content = fs6.readFileSync(repoGitignore, "utf-8");
1378
+ if (fs8.existsSync(repoGitignore)) {
1379
+ const content = fs8.readFileSync(repoGitignore, "utf-8");
922
1380
  for (const { entry, label } of gitignoreEntries) {
923
1381
  if (!content.includes(entry)) {
924
- fs6.appendFileSync(repoGitignore, `
1382
+ fs8.appendFileSync(repoGitignore, `
925
1383
  # ${label}
926
1384
  ${entry}
927
1385
  `);
@@ -960,15 +1418,18 @@ ${entry}
960
1418
  };
961
1419
  }
962
1420
 
1421
+ // src/commands/add.ts
1422
+ init_utils();
1423
+
963
1424
  // src/adapters/claude.ts
964
- import * as fs8 from "fs";
965
- import * as path8 from "path";
1425
+ import * as fs10 from "fs";
1426
+ import * as path9 from "path";
966
1427
  import { execSync } from "child_process";
967
1428
 
968
1429
  // src/adapters/common.ts
969
- import * as fs7 from "fs";
1430
+ import * as fs9 from "fs";
970
1431
  import * as os2 from "os";
971
- import * as path7 from "path";
1432
+ import * as path8 from "path";
972
1433
  import { spawnSync as spawnSync2 } from "child_process";
973
1434
  import { fileURLToPath as fileURLToPath2 } from "url";
974
1435
  function probeCommand(candidates) {
@@ -986,7 +1447,7 @@ function probeCommand(candidates) {
986
1447
  return { command: null, version: null };
987
1448
  }
988
1449
  function resolveCommandPath(command) {
989
- if (path7.isAbsolute(command)) return command;
1450
+ if (path8.isAbsolute(command)) return command;
990
1451
  const whichCmd = process.platform === "win32" ? "where.exe" : "which";
991
1452
  try {
992
1453
  const result = spawnSync2(whichCmd, [command], {
@@ -997,19 +1458,19 @@ function resolveCommandPath(command) {
997
1458
  const lines = result.stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
998
1459
  if (lines.length === 0) return null;
999
1460
  if (process.platform === "win32") {
1000
- const candidateExt = path7.extname(command).toLowerCase();
1461
+ const candidateExt = path8.extname(command).toLowerCase();
1001
1462
  if (candidateExt) {
1002
1463
  const extMatch = lines.find(
1003
- (l) => path7.extname(l).toLowerCase() === candidateExt && fs7.existsSync(l)
1464
+ (l) => path8.extname(l).toLowerCase() === candidateExt && fs9.existsSync(l)
1004
1465
  );
1005
1466
  if (extMatch) return extMatch;
1006
1467
  }
1007
1468
  const executableMatch = lines.find(
1008
- (l) => /\.(cmd|exe|ps1)$/i.test(l) && fs7.existsSync(l)
1469
+ (l) => /\.(cmd|exe|ps1)$/i.test(l) && fs9.existsSync(l)
1009
1470
  );
1010
1471
  if (executableMatch) return executableMatch;
1011
1472
  }
1012
- const firstValid = lines.find((l) => fs7.existsSync(l));
1473
+ const firstValid = lines.find((l) => fs9.existsSync(l));
1013
1474
  return firstValid ?? null;
1014
1475
  } catch {
1015
1476
  return null;
@@ -1019,17 +1480,17 @@ function getHomeDir() {
1019
1480
  return os2.homedir();
1020
1481
  }
1021
1482
  function toForwardSlashPath(filePath) {
1022
- return path7.resolve(filePath).replace(/\\/g, "/");
1483
+ return path8.resolve(filePath).replace(/\\/g, "/");
1023
1484
  }
1024
1485
  function canWriteOrCreate(filePath) {
1025
1486
  try {
1026
- if (fs7.existsSync(filePath)) {
1027
- fs7.accessSync(filePath, fs7.constants.W_OK);
1487
+ if (fs9.existsSync(filePath)) {
1488
+ fs9.accessSync(filePath, fs9.constants.W_OK);
1028
1489
  return true;
1029
1490
  }
1030
- const parent = path7.dirname(filePath);
1031
- fs7.mkdirSync(parent, { recursive: true });
1032
- fs7.accessSync(parent, fs7.constants.W_OK);
1491
+ const parent = path8.dirname(filePath);
1492
+ fs9.mkdirSync(parent, { recursive: true });
1493
+ fs9.accessSync(parent, fs9.constants.W_OK);
1033
1494
  return true;
1034
1495
  } catch {
1035
1496
  return false;
@@ -1041,14 +1502,14 @@ function isEphemeralPath(p) {
1041
1502
  }
1042
1503
  function findLocalTapCommsSource(ctx) {
1043
1504
  const candidates = [
1044
- path7.join(
1505
+ path8.join(
1045
1506
  ctx.repoRoot,
1046
1507
  "packages",
1047
1508
  "tap-plugin",
1048
1509
  "channels",
1049
1510
  "tap-comms.ts"
1050
1511
  ),
1051
- path7.join(
1512
+ path8.join(
1052
1513
  ctx.repoRoot,
1053
1514
  "node_modules",
1054
1515
  "@hua-labs",
@@ -1058,19 +1519,19 @@ function findLocalTapCommsSource(ctx) {
1058
1519
  )
1059
1520
  ];
1060
1521
  for (const candidate of candidates) {
1061
- if (fs7.existsSync(candidate)) return candidate;
1522
+ if (fs9.existsSync(candidate)) return candidate;
1062
1523
  }
1063
1524
  return null;
1064
1525
  }
1065
1526
  function findBundledTapCommsSource(metaUrl = import.meta.url) {
1066
- const moduleDir = path7.dirname(fileURLToPath2(metaUrl));
1527
+ const moduleDir = path8.dirname(fileURLToPath2(metaUrl));
1067
1528
  const candidates = [
1068
- path7.join(moduleDir, "mcp-server.mjs"),
1069
- path7.join(moduleDir, "..", "mcp-server.mjs"),
1070
- path7.join(moduleDir, "..", "mcp-server.ts")
1529
+ path8.join(moduleDir, "mcp-server.mjs"),
1530
+ path8.join(moduleDir, "..", "mcp-server.mjs"),
1531
+ path8.join(moduleDir, "..", "mcp-server.ts")
1071
1532
  ];
1072
1533
  for (const candidate of candidates) {
1073
- if (fs7.existsSync(candidate)) return candidate;
1534
+ if (fs9.existsSync(candidate)) return candidate;
1074
1535
  }
1075
1536
  return null;
1076
1537
  }
@@ -1079,15 +1540,15 @@ function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
1079
1540
  }
1080
1541
  function findPreferredBunCommand() {
1081
1542
  const home = getHomeDir();
1082
- const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
1543
+ const candidates = process.platform === "win32" ? [path8.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path8.join(home, ".bun", "bin", "bun"), "bun"];
1083
1544
  for (const candidate of candidates) {
1084
- if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
1545
+ if (path8.isAbsolute(candidate) && !fs9.existsSync(candidate)) continue;
1085
1546
  const result = spawnSync2(candidate, ["--version"], {
1086
1547
  encoding: "utf-8",
1087
1548
  shell: process.platform === "win32"
1088
1549
  });
1089
1550
  if (result.status === 0) {
1090
- return path7.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
1551
+ return path8.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
1091
1552
  }
1092
1553
  }
1093
1554
  return null;
@@ -1114,7 +1575,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1114
1575
  }
1115
1576
  const isBundled = sourcePath.endsWith(".mjs");
1116
1577
  const isEphemeralSource = isEphemeralPath(sourcePath);
1117
- let command = bunCommand;
1578
+ let command = null;
1118
1579
  let args = [toForwardSlashPath(sourcePath)];
1119
1580
  if (isEphemeralSource && isBundled) {
1120
1581
  command = "npx";
@@ -1122,23 +1583,17 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1122
1583
  warnings.push(
1123
1584
  "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
1124
1585
  );
1125
- } else if (!command && isBundled) {
1126
- const isEphemeralNode = isEphemeralPath(process.execPath);
1127
- if (isEphemeralNode) {
1128
- command = "node";
1129
- warnings.push(
1130
- "Detected ephemeral node path. Using `node` from PATH for MCP config stability."
1131
- );
1132
- } else {
1133
- command = toForwardSlashPath(process.execPath);
1134
- }
1135
- warnings.push(
1136
- "bun not found; using node to run the compiled MCP server. Install bun for better performance."
1586
+ } else if (isBundled) {
1587
+ const nodeProbe = probeCommand(
1588
+ process.platform === "win32" ? ["node", "node.exe"] : ["node"]
1137
1589
  );
1590
+ command = nodeProbe.command ?? "node";
1591
+ } else {
1592
+ command = bunCommand;
1138
1593
  }
1139
1594
  if (!command) {
1140
1595
  issues.push(
1141
- "bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
1596
+ isBundled ? "node is required to run the compiled MCP server (.mjs). Ensure node is in PATH." : "bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
1142
1597
  );
1143
1598
  return { command: null, args: [], env, sourcePath, warnings, issues };
1144
1599
  }
@@ -1156,7 +1611,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1156
1611
  var MCP_SERVER_KEY = "tap";
1157
1612
  var OLD_MCP_SERVER_KEY = "tap-comms";
1158
1613
  function findMcpJsonPath(ctx) {
1159
- return path8.join(ctx.repoRoot, ".mcp.json");
1614
+ return path9.join(ctx.repoRoot, ".mcp.json");
1160
1615
  }
1161
1616
  function findClaudeCommand() {
1162
1617
  try {
@@ -1182,11 +1637,11 @@ var claudeAdapter = {
1182
1637
  const warnings = [];
1183
1638
  const issues = [];
1184
1639
  const configPath = findMcpJsonPath(ctx);
1185
- const configExists = fs8.existsSync(configPath);
1640
+ const configExists = fs10.existsSync(configPath);
1186
1641
  const runtimeCommand = findClaudeCommand();
1187
1642
  const canWrite = configExists ? (() => {
1188
1643
  try {
1189
- fs8.accessSync(configPath, fs8.constants.W_OK);
1644
+ fs10.accessSync(configPath, fs10.constants.W_OK);
1190
1645
  return true;
1191
1646
  } catch {
1192
1647
  return false;
@@ -1200,7 +1655,7 @@ var claudeAdapter = {
1200
1655
  const managed = buildManagedMcpServerSpec(ctx);
1201
1656
  warnings.push(...managed.warnings);
1202
1657
  issues.push(...managed.issues);
1203
- if (!fs8.existsSync(ctx.commsDir)) {
1658
+ if (!fs10.existsSync(ctx.commsDir)) {
1204
1659
  issues.push(
1205
1660
  `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
1206
1661
  );
@@ -1224,7 +1679,7 @@ var claudeAdapter = {
1224
1679
  const operations = [];
1225
1680
  const ownedArtifacts = [];
1226
1681
  if (probe.configExists) {
1227
- const raw = fs8.readFileSync(configPath, "utf-8");
1682
+ const raw = fs10.readFileSync(configPath, "utf-8");
1228
1683
  try {
1229
1684
  const config = JSON.parse(raw);
1230
1685
  if (config.mcpServers?.[MCP_SERVER_KEY]) {
@@ -1288,9 +1743,9 @@ var claudeAdapter = {
1288
1743
  try {
1289
1744
  if (op.type === "set" || op.type === "merge") {
1290
1745
  let config = {};
1291
- if (fs8.existsSync(op.path)) {
1746
+ if (fs10.existsSync(op.path)) {
1292
1747
  backupFile(op.path, plan.backupDir);
1293
- const raw = fs8.readFileSync(op.path, "utf-8");
1748
+ const raw = fs10.readFileSync(op.path, "utf-8");
1294
1749
  try {
1295
1750
  config = JSON.parse(raw);
1296
1751
  } catch {
@@ -1307,12 +1762,12 @@ var claudeAdapter = {
1307
1762
  setNestedKey(config, op.key, op.value);
1308
1763
  }
1309
1764
  const tmp = `${op.path}.tmp.${process.pid}`;
1310
- fs8.writeFileSync(
1765
+ fs10.writeFileSync(
1311
1766
  tmp,
1312
1767
  JSON.stringify(config, null, 2) + "\n",
1313
1768
  "utf-8"
1314
1769
  );
1315
- fs8.renameSync(tmp, op.path);
1770
+ fs10.renameSync(tmp, op.path);
1316
1771
  changedFiles.push(op.path);
1317
1772
  appliedOps++;
1318
1773
  }
@@ -1341,12 +1796,12 @@ var claudeAdapter = {
1341
1796
  if (configPath) {
1342
1797
  checks.push({
1343
1798
  name: "Config file exists",
1344
- passed: fs8.existsSync(configPath),
1345
- message: fs8.existsSync(configPath) ? void 0 : `${configPath} not found`
1799
+ passed: fs10.existsSync(configPath),
1800
+ message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1346
1801
  });
1347
- if (fs8.existsSync(configPath)) {
1802
+ if (fs10.existsSync(configPath)) {
1348
1803
  try {
1349
- const raw = fs8.readFileSync(configPath, "utf-8");
1804
+ const raw = fs10.readFileSync(configPath, "utf-8");
1350
1805
  const config = JSON.parse(raw);
1351
1806
  checks.push({ name: "Config is valid JSON", passed: true });
1352
1807
  const entry = config.mcpServers?.[MCP_SERVER_KEY];
@@ -1374,8 +1829,8 @@ var claudeAdapter = {
1374
1829
  }
1375
1830
  checks.push({
1376
1831
  name: "Comms directory exists",
1377
- passed: fs8.existsSync(ctx.commsDir),
1378
- message: fs8.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1832
+ passed: fs10.existsSync(ctx.commsDir),
1833
+ message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1379
1834
  });
1380
1835
  const cmd = findClaudeCommand();
1381
1836
  checks.push({
@@ -1408,35 +1863,35 @@ function setNestedKey(obj, keyPath, value) {
1408
1863
  current[keys[keys.length - 1]] = value;
1409
1864
  }
1410
1865
  function normalizeTapCommsDir(value) {
1411
- return typeof value === "string" ? path8.resolve(value).replace(/\\/g, "/") : "";
1866
+ return typeof value === "string" ? path9.resolve(value).replace(/\\/g, "/") : "";
1412
1867
  }
1413
1868
 
1414
1869
  // src/adapters/codex.ts
1415
- import * as fs10 from "fs";
1416
- import * as path10 from "path";
1870
+ import * as fs12 from "fs";
1871
+ import * as path11 from "path";
1417
1872
  import { fileURLToPath as fileURLToPath3 } from "url";
1418
1873
 
1419
1874
  // src/artifact-backups.ts
1420
- import * as crypto2 from "crypto";
1421
- import * as fs9 from "fs";
1422
- import * as path9 from "path";
1875
+ import * as crypto3 from "crypto";
1876
+ import * as fs11 from "fs";
1877
+ import * as path10 from "path";
1423
1878
  function selectorHash(selector) {
1424
- return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
1879
+ return crypto3.createHash("sha256").update(selector).digest("hex").slice(0, 12);
1425
1880
  }
1426
1881
  function artifactBackupPath(backupDir, kind, selector) {
1427
1882
  const safeKind = kind.replace(/[^a-z-]/gi, "-");
1428
- return path9.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
1883
+ return path10.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
1429
1884
  }
1430
1885
  function writeArtifactBackup(backupPath, payload) {
1431
- fs9.mkdirSync(path9.dirname(backupPath), { recursive: true });
1886
+ fs11.mkdirSync(path10.dirname(backupPath), { recursive: true });
1432
1887
  const tmp = `${backupPath}.tmp.${process.pid}`;
1433
- fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
1434
- fs9.renameSync(tmp, backupPath);
1888
+ fs11.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
1889
+ fs11.renameSync(tmp, backupPath);
1435
1890
  }
1436
1891
  function readArtifactBackup(backupPath) {
1437
- if (!fs9.existsSync(backupPath)) return null;
1892
+ if (!fs11.existsSync(backupPath)) return null;
1438
1893
  try {
1439
- const raw = fs9.readFileSync(backupPath, "utf-8");
1894
+ const raw = fs11.readFileSync(backupPath, "utf-8");
1440
1895
  return JSON.parse(raw);
1441
1896
  } catch {
1442
1897
  return null;
@@ -1450,10 +1905,10 @@ var SESSION_NEUTRAL_AGENT_NAME = "<set-per-session>";
1450
1905
  var OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
1451
1906
  var OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
1452
1907
  function findCodexConfigPath2() {
1453
- return path10.join(getHomeDir(), ".codex", "config.toml");
1908
+ return path11.join(getHomeDir(), ".codex", "config.toml");
1454
1909
  }
1455
1910
  function canonicalizeTrustPath2(targetPath) {
1456
- let resolved = path10.resolve(targetPath).replace(/\//g, "\\");
1911
+ let resolved = path11.resolve(targetPath).replace(/\//g, "\\");
1457
1912
  const driveRoot = /^[A-Za-z]:\\$/;
1458
1913
  if (!driveRoot.test(resolved)) {
1459
1914
  resolved = resolved.replace(/\\+$/g, "");
@@ -1465,7 +1920,7 @@ function trustSelector(targetPath) {
1465
1920
  }
1466
1921
  function getTrustTargets(ctx) {
1467
1922
  const targets = [ctx.repoRoot, process.cwd()];
1468
- return [...new Set(targets.map((value) => path10.resolve(value)))];
1923
+ return [...new Set(targets.map((value) => path11.resolve(value)))];
1469
1924
  }
1470
1925
  function buildManagedArtifacts(configPath, ctx) {
1471
1926
  const artifacts = [
@@ -1482,14 +1937,14 @@ function buildManagedArtifacts(configPath, ctx) {
1482
1937
  return artifacts;
1483
1938
  }
1484
1939
  function readConfigOrEmpty(configPath) {
1485
- if (!fs10.existsSync(configPath)) return "";
1486
- return fs10.readFileSync(configPath, "utf-8");
1940
+ if (!fs12.existsSync(configPath)) return "";
1941
+ return fs12.readFileSync(configPath, "utf-8");
1487
1942
  }
1488
1943
  function writeTomlFile(filePath, content) {
1489
- fs10.mkdirSync(path10.dirname(filePath), { recursive: true });
1944
+ fs12.mkdirSync(path11.dirname(filePath), { recursive: true });
1490
1945
  const tmp = `${filePath}.tmp.${process.pid}`;
1491
- fs10.writeFileSync(tmp, content, "utf-8");
1492
- fs10.renameSync(tmp, filePath);
1946
+ fs12.writeFileSync(tmp, content, "utf-8");
1947
+ fs12.renameSync(tmp, filePath);
1493
1948
  }
1494
1949
  function buildSessionNeutralCodexSpec(ctx) {
1495
1950
  const managed = buildManagedMcpServerSpec(ctx);
@@ -1515,8 +1970,8 @@ function verifyManagedToml(content, ctx, configPath) {
1515
1970
  const envTable = extractTomlTable(content, ENV_SELECTOR);
1516
1971
  checks.push({
1517
1972
  name: "Codex config exists",
1518
- passed: fs10.existsSync(configPath),
1519
- message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1973
+ passed: fs12.existsSync(configPath),
1974
+ message: fs12.existsSync(configPath) ? void 0 : `${configPath} not found`
1520
1975
  });
1521
1976
  checks.push({
1522
1977
  name: "tap MCP table present",
@@ -1547,6 +2002,14 @@ function verifyManagedToml(content, ctx, configPath) {
1547
2002
  message: "Managed tap command/args do not match expected values"
1548
2003
  });
1549
2004
  }
2005
+ if (mainTable) {
2006
+ const mainValues = parseTomlAssignments(mainTable);
2007
+ checks.push({
2008
+ name: "approval_mode is auto",
2009
+ passed: mainValues.approval_mode === "auto",
2010
+ message: mainValues.approval_mode ? `approval_mode is "${mainValues.approval_mode}", expected "auto"` : 'approval_mode missing, expected "auto"'
2011
+ });
2012
+ }
1550
2013
  if (envTable) {
1551
2014
  const envValues = parseTomlAssignments(envTable);
1552
2015
  checks.push({
@@ -1568,7 +2031,7 @@ var codexAdapter = {
1568
2031
  const warnings = [];
1569
2032
  const issues = [];
1570
2033
  const configPath = findCodexConfigPath2();
1571
- const configExists = fs10.existsSync(configPath);
2034
+ const configExists = fs12.existsSync(configPath);
1572
2035
  const runtimeProbe = probeCommand(
1573
2036
  ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
1574
2037
  );
@@ -1577,7 +2040,7 @@ var codexAdapter = {
1577
2040
  "Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
1578
2041
  );
1579
2042
  }
1580
- if (!fs10.existsSync(ctx.commsDir)) {
2043
+ if (!fs12.existsSync(ctx.commsDir)) {
1581
2044
  issues.push(
1582
2045
  `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
1583
2046
  );
@@ -1658,7 +2121,7 @@ var codexAdapter = {
1658
2121
  };
1659
2122
  }
1660
2123
  const existingContent = readConfigOrEmpty(configPath);
1661
- if (fs10.existsSync(configPath) && existingContent) {
2124
+ if (fs12.existsSync(configPath) && existingContent) {
1662
2125
  backupFile(configPath, plan.backupDir);
1663
2126
  }
1664
2127
  const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
@@ -1690,7 +2153,8 @@ var codexAdapter = {
1690
2153
  MCP_SELECTOR,
1691
2154
  {
1692
2155
  command: managed.command,
1693
- args: managed.args
2156
+ args: managed.args,
2157
+ approval_mode: "auto"
1694
2158
  },
1695
2159
  extractTomlTable(existingContent, MCP_SELECTOR)
1696
2160
  )
@@ -1741,8 +2205,8 @@ var codexAdapter = {
1741
2205
  const checks = verifyManagedToml(content, ctx, configPath);
1742
2206
  checks.push({
1743
2207
  name: "Comms directory exists",
1744
- passed: fs10.existsSync(ctx.commsDir),
1745
- message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
2208
+ passed: fs12.existsSync(ctx.commsDir),
2209
+ message: fs12.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1746
2210
  });
1747
2211
  checks.push({
1748
2212
  name: "Codex CLI found",
@@ -1765,12 +2229,12 @@ var codexAdapter = {
1765
2229
  return "app-server";
1766
2230
  },
1767
2231
  resolveBridgeScript(ctx) {
1768
- const distDir = path10.dirname(fileURLToPath3(import.meta.url));
2232
+ const distDir = path11.dirname(fileURLToPath3(import.meta.url));
1769
2233
  const candidates = [
1770
2234
  // 1. Relative to bundled CLI (npm install / npx)
1771
- path10.join(distDir, "bridges", "codex-bridge-runner.mjs"),
2235
+ path11.join(distDir, "bridges", "codex-bridge-runner.mjs"),
1772
2236
  // 2. Monorepo development — dist inside repo
1773
- path10.join(
2237
+ path11.join(
1774
2238
  ctx.repoRoot,
1775
2239
  "packages",
1776
2240
  "tap-comms",
@@ -1779,7 +2243,7 @@ var codexAdapter = {
1779
2243
  "codex-bridge-runner.mjs"
1780
2244
  ),
1781
2245
  // 3. Source file — dev mode with strip-types
1782
- path10.join(
2246
+ path11.join(
1783
2247
  ctx.repoRoot,
1784
2248
  "packages",
1785
2249
  "tap-comms",
@@ -1789,31 +2253,47 @@ var codexAdapter = {
1789
2253
  )
1790
2254
  ];
1791
2255
  for (const candidate of candidates) {
1792
- if (fs10.existsSync(candidate)) return candidate;
2256
+ if (fs12.existsSync(candidate)) return candidate;
1793
2257
  }
1794
2258
  return null;
1795
2259
  }
1796
2260
  };
2261
+ function patchCodexApprovalMode() {
2262
+ const configPath = findCodexConfigPath2();
2263
+ if (!fs12.existsSync(configPath)) return null;
2264
+ const content = fs12.readFileSync(configPath, "utf-8");
2265
+ const tapTable = extractTomlTable(content, MCP_SELECTOR);
2266
+ if (!tapTable) return null;
2267
+ const values = parseTomlAssignments(tapTable);
2268
+ if (values.approval_mode === "auto") return null;
2269
+ const patched = replaceTomlTable(
2270
+ content,
2271
+ MCP_SELECTOR,
2272
+ renderTomlTable(MCP_SELECTOR, { approval_mode: "auto" }, tapTable)
2273
+ );
2274
+ writeTomlFile(configPath, patched);
2275
+ return configPath;
2276
+ }
1797
2277
 
1798
2278
  // src/adapters/gemini.ts
1799
- import * as fs11 from "fs";
1800
- import * as path11 from "path";
2279
+ import * as fs13 from "fs";
2280
+ import * as path12 from "path";
1801
2281
  var GEMINI_SELECTOR = "mcpServers.tap";
1802
2282
  var OLD_GEMINI_SELECTOR = "mcpServers.tap-comms";
1803
2283
  function candidateConfigPaths(ctx) {
1804
2284
  const home = getHomeDir();
1805
2285
  return [
1806
- path11.join(ctx.repoRoot, ".gemini", "settings.json"),
1807
- path11.join(home, ".gemini", "settings.json"),
1808
- path11.join(home, ".gemini", "antigravity", "mcp_config.json")
2286
+ path12.join(ctx.repoRoot, ".gemini", "settings.json"),
2287
+ path12.join(home, ".gemini", "settings.json"),
2288
+ path12.join(home, ".gemini", "antigravity", "mcp_config.json")
1809
2289
  ];
1810
2290
  }
1811
2291
  function chooseGeminiConfigPath(ctx) {
1812
2292
  const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
1813
- if (fs11.existsSync(workspaceConfig)) return workspaceConfig;
1814
- if (fs11.existsSync(homeConfig)) return homeConfig;
1815
- if (fs11.existsSync(antigravityConfig)) {
1816
- const raw = fs11.readFileSync(antigravityConfig, "utf-8").trim();
2293
+ if (fs13.existsSync(workspaceConfig)) return workspaceConfig;
2294
+ if (fs13.existsSync(homeConfig)) return homeConfig;
2295
+ if (fs13.existsSync(antigravityConfig)) {
2296
+ const raw = fs13.readFileSync(antigravityConfig, "utf-8").trim();
1817
2297
  if (raw) {
1818
2298
  try {
1819
2299
  JSON.parse(raw);
@@ -1825,8 +2305,8 @@ function chooseGeminiConfigPath(ctx) {
1825
2305
  return workspaceConfig;
1826
2306
  }
1827
2307
  function readJsonFile(filePath) {
1828
- if (!fs11.existsSync(filePath)) return {};
1829
- const raw = fs11.readFileSync(filePath, "utf-8").trim();
2308
+ if (!fs13.existsSync(filePath)) return {};
2309
+ const raw = fs13.readFileSync(filePath, "utf-8").trim();
1830
2310
  if (!raw) return {};
1831
2311
  return JSON.parse(raw);
1832
2312
  }
@@ -1857,8 +2337,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
1857
2337
  const entry = readNestedKey(config, GEMINI_SELECTOR);
1858
2338
  checks.push({
1859
2339
  name: "Gemini config exists",
1860
- passed: fs11.existsSync(configPath),
1861
- message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
2340
+ passed: fs13.existsSync(configPath),
2341
+ message: fs13.existsSync(configPath) ? void 0 : `${configPath} not found`
1862
2342
  });
1863
2343
  checks.push({
1864
2344
  name: "tap entry present",
@@ -1867,8 +2347,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
1867
2347
  });
1868
2348
  checks.push({
1869
2349
  name: "Comms directory exists",
1870
- passed: fs11.existsSync(ctx.commsDir),
1871
- message: fs11.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
2350
+ passed: fs13.existsSync(ctx.commsDir),
2351
+ message: fs13.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1872
2352
  });
1873
2353
  if (entry?.env && typeof entry.env === "object") {
1874
2354
  checks.push({
@@ -1885,7 +2365,7 @@ var geminiAdapter = {
1885
2365
  const warnings = [];
1886
2366
  const issues = [];
1887
2367
  const configPath = chooseGeminiConfigPath(ctx);
1888
- const configExists = fs11.existsSync(configPath);
2368
+ const configExists = fs13.existsSync(configPath);
1889
2369
  const runtimeProbe = probeCommand(
1890
2370
  ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
1891
2371
  );
@@ -1894,7 +2374,7 @@ var geminiAdapter = {
1894
2374
  "Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
1895
2375
  );
1896
2376
  }
1897
- if (!fs11.existsSync(ctx.commsDir)) {
2377
+ if (!fs13.existsSync(ctx.commsDir)) {
1898
2378
  issues.push(
1899
2379
  `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
1900
2380
  );
@@ -1973,8 +2453,8 @@ var geminiAdapter = {
1973
2453
  }
1974
2454
  let config = {};
1975
2455
  let previousValue = void 0;
1976
- if (fs11.existsSync(configPath)) {
1977
- if (fs11.readFileSync(configPath, "utf-8").trim()) {
2456
+ if (fs13.existsSync(configPath)) {
2457
+ if (fs13.readFileSync(configPath, "utf-8").trim()) {
1978
2458
  backupFile(configPath, plan.backupDir);
1979
2459
  }
1980
2460
  try {
@@ -2011,10 +2491,10 @@ var geminiAdapter = {
2011
2491
  args: managed.args,
2012
2492
  env: managed.env
2013
2493
  });
2014
- fs11.mkdirSync(path11.dirname(configPath), { recursive: true });
2494
+ fs13.mkdirSync(path12.dirname(configPath), { recursive: true });
2015
2495
  const tmp = `${configPath}.tmp.${process.pid}`;
2016
- fs11.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
2017
- fs11.renameSync(tmp, configPath);
2496
+ fs13.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
2497
+ fs13.renameSync(tmp, configPath);
2018
2498
  changedFiles.push(configPath);
2019
2499
  return {
2020
2500
  success: true,
@@ -2085,57 +2565,83 @@ function getAdapter(runtime) {
2085
2565
  }
2086
2566
 
2087
2567
  // src/engine/bridge-paths.ts
2088
- import * as path12 from "path";
2568
+ import * as path13 from "path";
2569
+ function assertPathContained(resolved, stateDir, subDir) {
2570
+ const expectedDir = path13.resolve(stateDir, subDir) + path13.sep;
2571
+ const normalizedResolved = path13.resolve(resolved);
2572
+ if (!normalizedResolved.startsWith(expectedDir)) {
2573
+ throw new Error(
2574
+ `Path traversal blocked: resolved path escapes "${subDir}/" directory`
2575
+ );
2576
+ }
2577
+ return normalizedResolved;
2578
+ }
2089
2579
  function appServerLogFilePath(stateDir, instanceId) {
2090
- return path12.join(stateDir, "logs", `app-server-${instanceId}.log`);
2580
+ return assertPathContained(
2581
+ path13.join(stateDir, "logs", `app-server-${instanceId}.log`),
2582
+ stateDir,
2583
+ "logs"
2584
+ );
2091
2585
  }
2092
2586
  function appServerGatewayLogFilePath(stateDir, instanceId) {
2093
- return path12.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
2587
+ return assertPathContained(
2588
+ path13.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`),
2589
+ stateDir,
2590
+ "logs"
2591
+ );
2094
2592
  }
2095
2593
  function appServerGatewayTokenFilePath(stateDir, instanceId) {
2096
- return path12.join(
2594
+ return assertPathContained(
2595
+ path13.join(stateDir, "secrets", `app-server-gateway-${instanceId}.token`),
2097
2596
  stateDir,
2098
- "secrets",
2099
- `app-server-gateway-${instanceId}.token`
2597
+ "secrets"
2100
2598
  );
2101
2599
  }
2102
2600
  function stderrLogFilePath(logPath) {
2103
2601
  return `${logPath}.stderr`;
2104
2602
  }
2105
2603
  function pidFilePath(stateDir, instanceId) {
2106
- return path12.join(stateDir, "pids", `bridge-${instanceId}.json`);
2604
+ return assertPathContained(
2605
+ path13.join(stateDir, "pids", `bridge-${instanceId}.json`),
2606
+ stateDir,
2607
+ "pids"
2608
+ );
2107
2609
  }
2108
2610
  function logFilePath(stateDir, instanceId) {
2109
- return path12.join(stateDir, "logs", `bridge-${instanceId}.log`);
2611
+ return assertPathContained(
2612
+ path13.join(stateDir, "logs", `bridge-${instanceId}.log`),
2613
+ stateDir,
2614
+ "logs"
2615
+ );
2110
2616
  }
2111
2617
  function runtimeHeartbeatFilePath(runtimeStateDir) {
2112
- return path12.join(runtimeStateDir, "heartbeat.json");
2618
+ return path13.join(runtimeStateDir, "heartbeat.json");
2113
2619
  }
2114
2620
  function runtimeThreadStateFilePath(runtimeStateDir) {
2115
- return path12.join(runtimeStateDir, "thread.json");
2621
+ return path13.join(runtimeStateDir, "thread.json");
2116
2622
  }
2117
2623
 
2118
2624
  // src/engine/bridge-file-io.ts
2119
- import * as fs12 from "fs";
2120
- import * as path13 from "path";
2625
+ import * as fs14 from "fs";
2626
+ import * as path14 from "path";
2121
2627
  var APP_SERVER_AUTH_FILE_MODE = 384;
2122
2628
  function writeProtectedTextFile(filePath, content) {
2123
- fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
2629
+ fs14.mkdirSync(path14.dirname(filePath), { recursive: true });
2124
2630
  const tmp = `${filePath}.tmp.${process.pid}`;
2125
- fs12.writeFileSync(tmp, content, {
2631
+ fs14.writeFileSync(tmp, content, {
2126
2632
  encoding: "utf-8",
2127
2633
  mode: APP_SERVER_AUTH_FILE_MODE
2128
2634
  });
2129
- fs12.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
2130
- fs12.renameSync(tmp, filePath);
2131
- fs12.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
2635
+ fs14.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
2636
+ fs14.renameSync(tmp, filePath);
2637
+ fs14.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
2132
2638
  }
2133
2639
  function removeFileIfExists(filePath) {
2134
- if (!filePath || !fs12.existsSync(filePath)) {
2640
+ if (!filePath || !fs14.existsSync(filePath)) {
2135
2641
  return;
2136
2642
  }
2137
2643
  try {
2138
- fs12.unlinkSync(filePath);
2644
+ fs14.unlinkSync(filePath);
2139
2645
  } catch {
2140
2646
  }
2141
2647
  }
@@ -2154,14 +2660,14 @@ function getWebSocketCtor() {
2154
2660
  return typeof candidate === "function" ? candidate : null;
2155
2661
  }
2156
2662
  function delay(ms) {
2157
- return new Promise((resolve14) => setTimeout(resolve14, ms));
2663
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
2158
2664
  }
2159
2665
  function isLoopbackHost(hostname) {
2160
2666
  return hostname === "127.0.0.1" || hostname === "localhost";
2161
2667
  }
2162
2668
  async function allocateLoopbackPort(hostname) {
2163
2669
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2164
- return await new Promise((resolve14, reject) => {
2670
+ return await new Promise((resolve15, reject) => {
2165
2671
  const server = net.createServer();
2166
2672
  server.unref();
2167
2673
  server.once("error", reject);
@@ -2179,19 +2685,19 @@ async function allocateLoopbackPort(hostname) {
2179
2685
  reject(error);
2180
2686
  return;
2181
2687
  }
2182
- resolve14(port);
2688
+ resolve15(port);
2183
2689
  });
2184
2690
  });
2185
2691
  });
2186
2692
  }
2187
2693
  async function isTcpPortAvailable(hostname, port) {
2188
2694
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2189
- return await new Promise((resolve14) => {
2695
+ return await new Promise((resolve15) => {
2190
2696
  const server = net.createServer();
2191
2697
  server.unref();
2192
- server.once("error", () => resolve14(false));
2698
+ server.once("error", () => resolve15(false));
2193
2699
  server.listen(port, bindHost, () => {
2194
- server.close((error) => resolve14(!error));
2700
+ server.close((error) => resolve15(!error));
2195
2701
  });
2196
2702
  });
2197
2703
  }
@@ -2243,8 +2749,8 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
2243
2749
  }
2244
2750
 
2245
2751
  // src/engine/bridge-codex-command.ts
2246
- import * as fs13 from "fs";
2247
- import * as path14 from "path";
2752
+ import * as fs15 from "fs";
2753
+ import * as path15 from "path";
2248
2754
  import { fileURLToPath as fileURLToPath4 } from "url";
2249
2755
  function resolveCodexCommand(platform) {
2250
2756
  const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
@@ -2259,20 +2765,18 @@ function resolveCodexCommand(platform) {
2259
2765
  function unwrapNpmCmdShim(cmdPath) {
2260
2766
  let content;
2261
2767
  try {
2262
- content = fs13.readFileSync(cmdPath, "utf-8");
2768
+ content = fs15.readFileSync(cmdPath, "utf-8");
2263
2769
  } catch {
2264
2770
  return null;
2265
2771
  }
2266
- const match = content.match(
2267
- /"%_prog%"\s+"(%dp0%\\[^"]+)"\s+%\*/
2268
- );
2772
+ const match = content.match(/"%_prog%"\s+"(%dp0%\\[^"]+)"\s+%\*/);
2269
2773
  if (!match) return null;
2270
- const dp0 = path14.dirname(cmdPath);
2774
+ const dp0 = path15.dirname(cmdPath);
2271
2775
  const scriptRelative = match[1].replace(/%dp0%\\/g, "");
2272
- const scriptPath = path14.resolve(dp0, scriptRelative);
2273
- if (!fs13.existsSync(scriptPath)) return null;
2274
- const localNode = path14.join(dp0, "node.exe");
2275
- const nodeCommand = fs13.existsSync(localNode) ? localNode : probeCommand(["node.exe", "node"]).command ?? "node";
2776
+ const scriptPath = path15.resolve(dp0, scriptRelative);
2777
+ if (!fs15.existsSync(scriptPath)) return null;
2778
+ const localNode = path15.join(dp0, "node.exe");
2779
+ const nodeCommand = fs15.existsSync(localNode) ? localNode : probeCommand(["node.exe", "node"]).command ?? "node";
2276
2780
  return `${nodeCommand}\0${scriptPath}`;
2277
2781
  }
2278
2782
  function splitResolvedCommand(resolved) {
@@ -2286,14 +2790,16 @@ function resolvePowerShellCommand() {
2286
2790
  return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
2287
2791
  }
2288
2792
  function resolveAuthGatewayScript(repoRoot) {
2289
- const moduleDir = path14.dirname(fileURLToPath4(import.meta.url));
2793
+ const moduleDir = path15.dirname(fileURLToPath4(import.meta.url));
2794
+ const resolvedModuleDir = path15.resolve(moduleDir);
2795
+ const resolvedRepoRoot = path15.resolve(repoRoot);
2290
2796
  const candidates = [
2291
2797
  // Bundled: dist/bridges/ sibling (npm install / built package)
2292
- path14.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
2798
+ path15.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
2293
2799
  // Source: src/bridges/ sibling (monorepo dev with ts runner)
2294
- path14.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
2800
+ path15.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
2295
2801
  // Monorepo dist fallback
2296
- path14.join(
2802
+ path15.join(
2297
2803
  repoRoot,
2298
2804
  "packages",
2299
2805
  "tap-comms",
@@ -2301,7 +2807,7 @@ function resolveAuthGatewayScript(repoRoot) {
2301
2807
  "bridges",
2302
2808
  "codex-app-server-auth-gateway.mjs"
2303
2809
  ),
2304
- path14.join(
2810
+ path15.join(
2305
2811
  repoRoot,
2306
2812
  "packages",
2307
2813
  "tap-comms",
@@ -2311,17 +2817,21 @@ function resolveAuthGatewayScript(repoRoot) {
2311
2817
  )
2312
2818
  ];
2313
2819
  for (const candidate of candidates) {
2314
- if (fs13.existsSync(candidate)) {
2315
- return candidate;
2820
+ const resolved = path15.resolve(candidate);
2821
+ if (!resolved.startsWith(resolvedModuleDir + path15.sep) && !resolved.startsWith(resolvedRepoRoot + path15.sep)) {
2822
+ continue;
2823
+ }
2824
+ if (fs15.existsSync(resolved)) {
2825
+ return resolved;
2316
2826
  }
2317
2827
  }
2318
2828
  return null;
2319
2829
  }
2320
2830
 
2321
2831
  // src/engine/bridge-windows-spawn.ts
2322
- import * as fs14 from "fs";
2832
+ import * as fs16 from "fs";
2323
2833
  import * as os3 from "os";
2324
- import * as path15 from "path";
2834
+ import * as path16 from "path";
2325
2835
  import { randomBytes } from "crypto";
2326
2836
  import { spawnSync as spawnSync3 } from "child_process";
2327
2837
  var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
@@ -2329,7 +2839,7 @@ var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
2329
2839
  function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
2330
2840
  let entries;
2331
2841
  try {
2332
- entries = fs14.readdirSync(os3.tmpdir());
2842
+ entries = fs16.readdirSync(os3.tmpdir());
2333
2843
  } catch {
2334
2844
  return;
2335
2845
  }
@@ -2337,13 +2847,13 @@ function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
2337
2847
  if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
2338
2848
  continue;
2339
2849
  }
2340
- const wrapperPath = path15.join(os3.tmpdir(), entry);
2850
+ const wrapperPath = path16.join(os3.tmpdir(), entry);
2341
2851
  try {
2342
- const stats = fs14.statSync(wrapperPath);
2852
+ const stats = fs16.statSync(wrapperPath);
2343
2853
  if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
2344
2854
  continue;
2345
2855
  }
2346
- fs14.unlinkSync(wrapperPath);
2856
+ fs16.unlinkSync(wrapperPath);
2347
2857
  } catch {
2348
2858
  }
2349
2859
  }
@@ -2378,11 +2888,11 @@ function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = pro
2378
2888
  const stderrLogPath = stderrLogFilePath(logPath);
2379
2889
  const powerShellCommand = resolvePowerShellCommand();
2380
2890
  cleanupStaleWindowsSpawnWrappers();
2381
- const wrapperPath = path15.join(
2891
+ const wrapperPath = path16.join(
2382
2892
  os3.tmpdir(),
2383
2893
  `${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
2384
2894
  );
2385
- fs14.writeFileSync(
2895
+ fs16.writeFileSync(
2386
2896
  wrapperPath,
2387
2897
  buildWindowsDetachedWrapperScript(
2388
2898
  command,
@@ -2468,7 +2978,7 @@ function findListeningProcessId(url, platform) {
2468
2978
  }
2469
2979
 
2470
2980
  // src/engine/bridge-unix-spawn.ts
2471
- import * as fs15 from "fs";
2981
+ import * as fs17 from "fs";
2472
2982
  import { spawn, spawnSync as spawnSync4 } from "child_process";
2473
2983
  var DEFAULT_UNIX_PLATFORM = process.platform === "darwin" ? "darwin" : "linux";
2474
2984
  function resolveUnixSpawnCommand(command, args, platform) {
@@ -2515,8 +3025,8 @@ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = proces
2515
3025
  let logFd = null;
2516
3026
  let stderrFd = null;
2517
3027
  try {
2518
- logFd = fs15.openSync(logPath, "a");
2519
- stderrFd = fs15.openSync(stderrPath, "a");
3028
+ logFd = fs17.openSync(logPath, "a");
3029
+ stderrFd = fs17.openSync(stderrPath, "a");
2520
3030
  const launch = resolveUnixSpawnCommand(command, args, platform);
2521
3031
  const child = spawn(launch.command, launch.args, {
2522
3032
  cwd: repoRoot,
@@ -2529,10 +3039,10 @@ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = proces
2529
3039
  return child.pid ?? null;
2530
3040
  } finally {
2531
3041
  if (logFd != null) {
2532
- fs15.closeSync(logFd);
3042
+ fs17.closeSync(logFd);
2533
3043
  }
2534
3044
  if (stderrFd != null) {
2535
- fs15.closeSync(stderrFd);
3045
+ fs17.closeSync(stderrFd);
2536
3046
  }
2537
3047
  }
2538
3048
  }
@@ -2638,10 +3148,18 @@ async function stopManagedAppServer(appServer, platform) {
2638
3148
  }
2639
3149
 
2640
3150
  // src/engine/bridge-config.ts
2641
- import * as fs16 from "fs";
2642
- import * as path16 from "path";
3151
+ import * as fs18 from "fs";
3152
+ import * as path17 from "path";
3153
+ init_instance_config();
2643
3154
  function resolveAgentName(instanceId, explicit, context) {
2644
3155
  if (explicit) return explicit;
3156
+ if (context?.stateDir) {
3157
+ try {
3158
+ const instConfig = loadInstanceConfig(context.stateDir, instanceId);
3159
+ if (instConfig?.agentName) return instConfig.agentName;
3160
+ } catch {
3161
+ }
3162
+ }
2645
3163
  try {
2646
3164
  const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
2647
3165
  const state = loadState(repoRoot);
@@ -2660,13 +3178,13 @@ function inferRestartMode(bridgeState, flags, savedMode) {
2660
3178
  }
2661
3179
  function cleanupHeadlessDispatch(inboxDir, agentName) {
2662
3180
  const removed = [];
2663
- if (!fs16.existsSync(inboxDir)) return removed;
3181
+ if (!fs18.existsSync(inboxDir)) return removed;
2664
3182
  const normalizedAgent = agentName.replace(/-/g, "_");
2665
3183
  const marker = `-headless-${normalizedAgent}-review-`;
2666
3184
  try {
2667
- for (const file of fs16.readdirSync(inboxDir)) {
3185
+ for (const file of fs18.readdirSync(inboxDir)) {
2668
3186
  if (file.includes(marker)) {
2669
- fs16.unlinkSync(path16.join(inboxDir, file));
3187
+ fs18.unlinkSync(path17.join(inboxDir, file));
2670
3188
  removed.push(file);
2671
3189
  }
2672
3190
  }
@@ -2676,19 +3194,31 @@ function cleanupHeadlessDispatch(inboxDir, agentName) {
2676
3194
  }
2677
3195
 
2678
3196
  // src/engine/bridge-state.ts
2679
- import * as fs17 from "fs";
3197
+ import * as fs19 from "fs";
3198
+ function transitionBridgeLifecycle(previous, nextState, reason, options) {
3199
+ const at = options?.at ?? (/* @__PURE__ */ new Date()).toISOString();
3200
+ const changed = previous?.state !== nextState;
3201
+ return {
3202
+ state: nextState,
3203
+ since: changed || !previous?.since ? at : previous.since,
3204
+ updatedAt: at,
3205
+ lastTransitionAt: changed || !previous?.lastTransitionAt ? at : previous.lastTransitionAt,
3206
+ lastTransitionReason: changed || previous?.lastTransitionReason == null ? reason : previous.lastTransitionReason,
3207
+ restartCount: (previous?.restartCount ?? 0) + (options?.incrementRestart ? 1 : 0)
3208
+ };
3209
+ }
2680
3210
  function loadRuntimeBridgeHeartbeat(bridgeState) {
2681
3211
  const runtimeStateDir = bridgeState?.runtimeStateDir;
2682
3212
  if (!runtimeStateDir) {
2683
3213
  return null;
2684
3214
  }
2685
3215
  const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
2686
- if (!fs17.existsSync(heartbeatPath)) {
3216
+ if (!fs19.existsSync(heartbeatPath)) {
2687
3217
  return null;
2688
3218
  }
2689
3219
  try {
2690
3220
  return JSON.parse(
2691
- fs17.readFileSync(heartbeatPath, "utf-8")
3221
+ fs19.readFileSync(heartbeatPath, "utf-8")
2692
3222
  );
2693
3223
  } catch {
2694
3224
  return null;
@@ -2700,12 +3230,12 @@ function loadRuntimeBridgeThreadState(bridgeState) {
2700
3230
  return null;
2701
3231
  }
2702
3232
  const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
2703
- if (!fs17.existsSync(threadPath)) {
3233
+ if (!fs19.existsSync(threadPath)) {
2704
3234
  return null;
2705
3235
  }
2706
3236
  try {
2707
3237
  const parsed = JSON.parse(
2708
- fs17.readFileSync(threadPath, "utf-8")
3238
+ fs19.readFileSync(threadPath, "utf-8")
2709
3239
  );
2710
3240
  return parsed.threadId ? parsed : null;
2711
3241
  } catch {
@@ -2714,9 +3244,9 @@ function loadRuntimeBridgeThreadState(bridgeState) {
2714
3244
  }
2715
3245
  function loadBridgeState(stateDir, instanceId) {
2716
3246
  const pidPath = pidFilePath(stateDir, instanceId);
2717
- if (!fs17.existsSync(pidPath)) return null;
3247
+ if (!fs19.existsSync(pidPath)) return null;
2718
3248
  try {
2719
- const raw = fs17.readFileSync(pidPath, "utf-8");
3249
+ const raw = fs19.readFileSync(pidPath, "utf-8");
2720
3250
  return JSON.parse(raw);
2721
3251
  } catch {
2722
3252
  return null;
@@ -2732,8 +3262,8 @@ function saveBridgeState(stateDir, instanceId, state) {
2732
3262
  }
2733
3263
  function clearBridgeState(stateDir, instanceId) {
2734
3264
  const pidPath = pidFilePath(stateDir, instanceId);
2735
- if (fs17.existsSync(pidPath)) {
2736
- fs17.unlinkSync(pidPath);
3265
+ if (fs19.existsSync(pidPath)) {
3266
+ fs19.unlinkSync(pidPath);
2737
3267
  }
2738
3268
  }
2739
3269
  function isBridgeRunning(stateDir, instanceId) {
@@ -2743,7 +3273,7 @@ function isBridgeRunning(stateDir, instanceId) {
2743
3273
  }
2744
3274
 
2745
3275
  // src/engine/bridge-observability.ts
2746
- import * as fs18 from "fs";
3276
+ import * as fs20 from "fs";
2747
3277
  function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
2748
3278
  const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
2749
3279
  return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
@@ -2795,16 +3325,251 @@ function isTurnStuck(stateDir, instanceId, thresholdSeconds = 300) {
2795
3325
  return info?.stuck ?? false;
2796
3326
  }
2797
3327
  function rotateLog(logPath) {
2798
- if (!fs18.existsSync(logPath)) return;
3328
+ if (!fs20.existsSync(logPath)) return;
2799
3329
  try {
2800
- const stats = fs18.statSync(logPath);
3330
+ const stats = fs20.statSync(logPath);
2801
3331
  if (stats.size === 0) return;
2802
3332
  const prevPath = `${logPath}.prev`;
2803
- fs18.renameSync(logPath, prevPath);
3333
+ fs20.renameSync(logPath, prevPath);
2804
3334
  } catch {
2805
3335
  }
2806
3336
  }
2807
3337
 
3338
+ // src/engine/server-lifecycle.ts
3339
+ function lifecycleMeta(persistedLifecycle) {
3340
+ return {
3341
+ lastTransitionAt: persistedLifecycle?.lastTransitionAt ?? null,
3342
+ lastTransitionReason: persistedLifecycle?.lastTransitionReason ?? null,
3343
+ restartCount: persistedLifecycle?.restartCount ?? 0
3344
+ };
3345
+ }
3346
+ function resolveBridgeLifecycleSnapshot(stateDir, instanceId, fallbackBridgeState, persistedLifecycle) {
3347
+ const persistedBridgeState = loadBridgeState(stateDir, instanceId) ?? fallbackBridgeState ?? null;
3348
+ const bridgeStatus = getBridgeStatus(stateDir, instanceId);
3349
+ const bridgeState = bridgeStatus === "running" ? loadBridgeState(stateDir, instanceId) ?? persistedBridgeState : persistedBridgeState;
3350
+ return deriveBridgeLifecycleState({
3351
+ bridgeStatus,
3352
+ bridgeState,
3353
+ runtimeHeartbeat: loadRuntimeBridgeHeartbeat(bridgeState),
3354
+ savedThread: loadRuntimeBridgeThreadState(bridgeState),
3355
+ persistedLifecycle
3356
+ });
3357
+ }
3358
+ function deriveBridgeLifecycleState(options) {
3359
+ const runtimeHeartbeat = options.runtimeHeartbeat ?? null;
3360
+ const savedThread = options.savedThread ?? null;
3361
+ const meta = lifecycleMeta(
3362
+ options.persistedLifecycle ?? options.bridgeState?.lifecycle ?? null
3363
+ );
3364
+ if (options.bridgeStatus === "stopped") {
3365
+ return {
3366
+ presence: "stopped",
3367
+ status: "stopped",
3368
+ summary: "stopped",
3369
+ ...meta,
3370
+ threadId: null,
3371
+ threadCwd: null,
3372
+ savedThreadId: savedThread?.threadId ?? null,
3373
+ savedThreadCwd: savedThread?.cwd ?? null,
3374
+ activeTurnId: null,
3375
+ connected: null,
3376
+ initialized: null,
3377
+ appServerHealthy: options.bridgeState?.appServer?.healthy ?? null
3378
+ };
3379
+ }
3380
+ if (options.bridgeStatus === "stale") {
3381
+ return {
3382
+ presence: "bridge-stale",
3383
+ status: "bridge-stale",
3384
+ summary: "bridge-stale",
3385
+ ...meta,
3386
+ threadId: runtimeHeartbeat?.threadId ?? null,
3387
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
3388
+ savedThreadId: savedThread?.threadId ?? null,
3389
+ savedThreadCwd: savedThread?.cwd ?? null,
3390
+ activeTurnId: runtimeHeartbeat?.activeTurnId ?? null,
3391
+ connected: runtimeHeartbeat?.connected ?? null,
3392
+ initialized: runtimeHeartbeat?.initialized ?? null,
3393
+ appServerHealthy: options.bridgeState?.appServer?.healthy ?? null
3394
+ };
3395
+ }
3396
+ const appServerHealthy = options.bridgeState?.appServer?.healthy ?? null;
3397
+ const threadId = runtimeHeartbeat?.threadId ?? null;
3398
+ const threadCwd = runtimeHeartbeat?.threadCwd ?? null;
3399
+ const connected = runtimeHeartbeat?.connected ?? null;
3400
+ const initialized = runtimeHeartbeat?.initialized ?? null;
3401
+ if (!runtimeHeartbeat) {
3402
+ return {
3403
+ presence: "bridge-live",
3404
+ status: "initializing",
3405
+ summary: "bridge-live, initializing",
3406
+ ...meta,
3407
+ threadId: null,
3408
+ threadCwd: null,
3409
+ savedThreadId: savedThread?.threadId ?? null,
3410
+ savedThreadCwd: savedThread?.cwd ?? null,
3411
+ activeTurnId: null,
3412
+ connected: null,
3413
+ initialized: null,
3414
+ appServerHealthy
3415
+ };
3416
+ }
3417
+ if (initialized === false) {
3418
+ return {
3419
+ presence: "bridge-live",
3420
+ status: "initializing",
3421
+ summary: "bridge-live, initializing",
3422
+ ...meta,
3423
+ threadId,
3424
+ threadCwd,
3425
+ savedThreadId: savedThread?.threadId ?? null,
3426
+ savedThreadCwd: savedThread?.cwd ?? null,
3427
+ activeTurnId: runtimeHeartbeat.activeTurnId ?? null,
3428
+ connected,
3429
+ initialized,
3430
+ appServerHealthy
3431
+ };
3432
+ }
3433
+ if (threadId && connected !== false) {
3434
+ return {
3435
+ presence: "bridge-live",
3436
+ status: "ready",
3437
+ summary: "bridge-live, ready",
3438
+ ...meta,
3439
+ threadId,
3440
+ threadCwd,
3441
+ savedThreadId: savedThread?.threadId ?? null,
3442
+ savedThreadCwd: savedThread?.cwd ?? null,
3443
+ activeTurnId: runtimeHeartbeat.activeTurnId ?? null,
3444
+ connected,
3445
+ initialized,
3446
+ appServerHealthy
3447
+ };
3448
+ }
3449
+ const degradedReason = savedThread?.threadId ? "saved thread only" : connected === false ? "disconnected" : "no active thread";
3450
+ return {
3451
+ presence: "bridge-live",
3452
+ status: "degraded-no-thread",
3453
+ summary: `bridge-live, degraded-no-thread (${degradedReason})`,
3454
+ ...meta,
3455
+ threadId,
3456
+ threadCwd,
3457
+ savedThreadId: savedThread?.threadId ?? null,
3458
+ savedThreadCwd: savedThread?.cwd ?? null,
3459
+ activeTurnId: runtimeHeartbeat.activeTurnId ?? null,
3460
+ connected,
3461
+ initialized,
3462
+ appServerHealthy
3463
+ };
3464
+ }
3465
+
3466
+ // src/engine/codex-session-state.ts
3467
+ import * as fs21 from "fs";
3468
+ import * as path18 from "path";
3469
+ function readLastDispatchAt(runtimeStateDir) {
3470
+ if (!runtimeStateDir) return null;
3471
+ const filePath = path18.join(runtimeStateDir, "last-dispatch.json");
3472
+ if (!fs21.existsSync(filePath)) return null;
3473
+ try {
3474
+ const parsed = JSON.parse(
3475
+ fs21.readFileSync(filePath, "utf-8")
3476
+ );
3477
+ return typeof parsed.dispatchedAt === "string" ? parsed.dispatchedAt : null;
3478
+ } catch {
3479
+ return null;
3480
+ }
3481
+ }
3482
+ function formatIdleSummary(idleSince) {
3483
+ if (!idleSince) return "idle";
3484
+ return `idle since ${idleSince}`;
3485
+ }
3486
+ function deriveCodexSessionState(options) {
3487
+ const runtimeHeartbeat = options.runtimeHeartbeat ?? null;
3488
+ if (!runtimeHeartbeat) {
3489
+ return {
3490
+ status: "initializing",
3491
+ turnState: null,
3492
+ summary: "initializing",
3493
+ activeTurnId: null,
3494
+ lastTurnAt: null,
3495
+ lastDispatchAt: null,
3496
+ idleSince: null,
3497
+ connected: null,
3498
+ initialized: null
3499
+ };
3500
+ }
3501
+ const turnState = runtimeHeartbeat.turnState ?? null;
3502
+ const activeTurnId = runtimeHeartbeat.activeTurnId ?? null;
3503
+ const lastTurnAt = runtimeHeartbeat.lastTurnAt ?? null;
3504
+ const lastDispatchAt = runtimeHeartbeat.lastDispatchAt ?? readLastDispatchAt(options.runtimeStateDir) ?? null;
3505
+ const idleSince = runtimeHeartbeat.idleSince ?? null;
3506
+ const connected = runtimeHeartbeat.connected ?? null;
3507
+ const initialized = runtimeHeartbeat.initialized ?? null;
3508
+ if (initialized === false) {
3509
+ return {
3510
+ status: "initializing",
3511
+ turnState,
3512
+ summary: "initializing",
3513
+ activeTurnId,
3514
+ lastTurnAt,
3515
+ lastDispatchAt,
3516
+ idleSince,
3517
+ connected,
3518
+ initialized
3519
+ };
3520
+ }
3521
+ if (turnState === "active" || activeTurnId) {
3522
+ return {
3523
+ status: "active",
3524
+ turnState: "active",
3525
+ summary: activeTurnId ? `active turn ${activeTurnId}` : "active",
3526
+ activeTurnId,
3527
+ lastTurnAt,
3528
+ lastDispatchAt,
3529
+ idleSince: null,
3530
+ connected,
3531
+ initialized
3532
+ };
3533
+ }
3534
+ if (turnState === "waiting-approval") {
3535
+ return {
3536
+ status: "waiting-approval",
3537
+ turnState,
3538
+ summary: `waiting-approval (${formatIdleSummary(idleSince)})`,
3539
+ activeTurnId,
3540
+ lastTurnAt,
3541
+ lastDispatchAt,
3542
+ idleSince,
3543
+ connected,
3544
+ initialized
3545
+ };
3546
+ }
3547
+ if (turnState === "disconnected" || connected === false) {
3548
+ return {
3549
+ status: "disconnected",
3550
+ turnState: "disconnected",
3551
+ summary: "disconnected",
3552
+ activeTurnId,
3553
+ lastTurnAt,
3554
+ lastDispatchAt,
3555
+ idleSince: null,
3556
+ connected,
3557
+ initialized
3558
+ };
3559
+ }
3560
+ return {
3561
+ status: "idle",
3562
+ turnState: turnState === "idle" ? turnState : "idle",
3563
+ summary: formatIdleSummary(idleSince),
3564
+ activeTurnId,
3565
+ lastTurnAt,
3566
+ lastDispatchAt,
3567
+ idleSince,
3568
+ connected,
3569
+ initialized
3570
+ };
3571
+ }
3572
+
2808
3573
  // src/engine/bridge-app-server-health.ts
2809
3574
  import * as net2 from "net";
2810
3575
  var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
@@ -2816,7 +3581,7 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2816
3581
  if (!WebSocket) {
2817
3582
  return false;
2818
3583
  }
2819
- return new Promise((resolve14) => {
3584
+ return new Promise((resolve15) => {
2820
3585
  let settled = false;
2821
3586
  let socket = null;
2822
3587
  const finish = (healthy) => {
@@ -2829,7 +3594,7 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2829
3594
  socket?.close();
2830
3595
  } catch {
2831
3596
  }
2832
- resolve14(healthy);
3597
+ resolve15(healthy);
2833
3598
  };
2834
3599
  const timer = setTimeout(() => finish(false), timeoutMs);
2835
3600
  try {
@@ -2901,21 +3666,21 @@ async function checkTcpPortListening(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_
2901
3666
  return false;
2902
3667
  }
2903
3668
  if (!port || !Number.isFinite(port)) return false;
2904
- return new Promise((resolve14) => {
3669
+ return new Promise((resolve15) => {
2905
3670
  const socket = net2.createConnection({ host: hostname, port });
2906
3671
  const timer = setTimeout(() => {
2907
3672
  socket.destroy();
2908
- resolve14(false);
3673
+ resolve15(false);
2909
3674
  }, timeoutMs);
2910
3675
  socket.once("connect", () => {
2911
3676
  clearTimeout(timer);
2912
3677
  socket.destroy();
2913
- resolve14(true);
3678
+ resolve15(true);
2914
3679
  });
2915
3680
  socket.once("error", () => {
2916
3681
  clearTimeout(timer);
2917
3682
  socket.destroy();
2918
- resolve14(false);
3683
+ resolve15(false);
2919
3684
  });
2920
3685
  });
2921
3686
  }
@@ -2954,19 +3719,19 @@ function markAppServerHealthy(appServer) {
2954
3719
  }
2955
3720
 
2956
3721
  // src/engine/bridge-app-server-auth.ts
2957
- import * as fs20 from "fs";
2958
- import * as path18 from "path";
3722
+ import * as fs23 from "fs";
3723
+ import * as path20 from "path";
2959
3724
  import { randomBytes as randomBytes2 } from "crypto";
2960
3725
 
2961
3726
  // src/runtime/resolve-node.ts
2962
- import * as fs19 from "fs";
2963
- import * as path17 from "path";
3727
+ import * as fs22 from "fs";
3728
+ import * as path19 from "path";
2964
3729
  import { execSync as execSync3 } from "child_process";
2965
3730
  function readNodeVersion(repoRoot) {
2966
- const nvFile = path17.join(repoRoot, ".node-version");
2967
- if (!fs19.existsSync(nvFile)) return null;
3731
+ const nvFile = path19.join(repoRoot, ".node-version");
3732
+ if (!fs22.existsSync(nvFile)) return null;
2968
3733
  try {
2969
- const raw = fs19.readFileSync(nvFile, "utf-8").trim();
3734
+ const raw = fs22.readFileSync(nvFile, "utf-8").trim();
2970
3735
  return raw.length > 0 ? raw.replace(/^v/, "") : null;
2971
3736
  } catch {
2972
3737
  return null;
@@ -2976,16 +3741,16 @@ function fnmCandidateDirs() {
2976
3741
  if (process.platform === "win32") {
2977
3742
  return [
2978
3743
  process.env.FNM_DIR,
2979
- process.env.APPDATA ? path17.join(process.env.APPDATA, "fnm") : null,
2980
- process.env.LOCALAPPDATA ? path17.join(process.env.LOCALAPPDATA, "fnm") : null,
2981
- process.env.USERPROFILE ? path17.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
3744
+ process.env.APPDATA ? path19.join(process.env.APPDATA, "fnm") : null,
3745
+ process.env.LOCALAPPDATA ? path19.join(process.env.LOCALAPPDATA, "fnm") : null,
3746
+ process.env.USERPROFILE ? path19.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
2982
3747
  ].filter(Boolean);
2983
3748
  }
2984
3749
  return [
2985
3750
  process.env.FNM_DIR,
2986
- process.env.HOME ? path17.join(process.env.HOME, ".local", "share", "fnm") : null,
2987
- process.env.HOME ? path17.join(process.env.HOME, ".fnm") : null,
2988
- process.env.XDG_DATA_HOME ? path17.join(process.env.XDG_DATA_HOME, "fnm") : null
3751
+ process.env.HOME ? path19.join(process.env.HOME, ".local", "share", "fnm") : null,
3752
+ process.env.HOME ? path19.join(process.env.HOME, ".fnm") : null,
3753
+ process.env.XDG_DATA_HOME ? path19.join(process.env.XDG_DATA_HOME, "fnm") : null
2989
3754
  ].filter(Boolean);
2990
3755
  }
2991
3756
  function nodeExecutableName() {
@@ -2995,14 +3760,14 @@ function probeFnmNode(desiredVersion) {
2995
3760
  const dirs = fnmCandidateDirs();
2996
3761
  const exe = nodeExecutableName();
2997
3762
  for (const baseDir of dirs) {
2998
- const candidate = path17.join(
3763
+ const candidate = path19.join(
2999
3764
  baseDir,
3000
3765
  "node-versions",
3001
3766
  `v${desiredVersion}`,
3002
3767
  "installation",
3003
3768
  exe
3004
3769
  );
3005
- if (!fs19.existsSync(candidate)) continue;
3770
+ if (!fs22.existsSync(candidate)) continue;
3006
3771
  try {
3007
3772
  const v = execSync3(`"${candidate}" --version`, {
3008
3773
  encoding: "utf-8",
@@ -3043,12 +3808,12 @@ function checkStripTypesSupport(command) {
3043
3808
  }
3044
3809
  function findTsxFallback(repoRoot) {
3045
3810
  const candidates = [
3046
- path17.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
3047
- path17.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
3048
- path17.join(repoRoot, "node_modules", ".bin", "tsx")
3811
+ path19.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
3812
+ path19.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
3813
+ path19.join(repoRoot, "node_modules", ".bin", "tsx")
3049
3814
  ];
3050
3815
  for (const c of candidates) {
3051
- if (fs19.existsSync(c)) return c;
3816
+ if (fs22.existsSync(c)) return c;
3052
3817
  }
3053
3818
  return null;
3054
3819
  }
@@ -3057,7 +3822,7 @@ function getFnmBinDir(repoRoot) {
3057
3822
  if (!desiredVersion) return null;
3058
3823
  const nodePath = probeFnmNode(desiredVersion);
3059
3824
  if (!nodePath) return null;
3060
- return path17.dirname(nodePath);
3825
+ return path19.dirname(nodePath);
3061
3826
  }
3062
3827
  function resolveNodeRuntime(configCommand, repoRoot) {
3063
3828
  if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
@@ -3113,7 +3878,7 @@ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
3113
3878
  const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
3114
3879
  return {
3115
3880
  ...baseEnv,
3116
- [pathKey]: `${fnmBin}${path17.delimiter}${currentPath}`
3881
+ [pathKey]: `${fnmBin}${path19.delimiter}${currentPath}`
3117
3882
  };
3118
3883
  }
3119
3884
 
@@ -3122,7 +3887,7 @@ function buildProtectedAppServerUrl(publicUrl, _token) {
3122
3887
  return publicUrl;
3123
3888
  }
3124
3889
  function readGatewayTokenFromPath(tokenPath) {
3125
- return fs20.readFileSync(tokenPath, "utf8").trim();
3890
+ return fs23.readFileSync(tokenPath, "utf8").trim();
3126
3891
  }
3127
3892
  function readGatewayToken(auth) {
3128
3893
  if (!auth) {
@@ -3132,14 +3897,14 @@ function readGatewayToken(auth) {
3132
3897
  if (legacyToken?.trim()) {
3133
3898
  return legacyToken.trim();
3134
3899
  }
3135
- if (!auth.tokenPath || !fs20.existsSync(auth.tokenPath)) {
3900
+ if (!auth.tokenPath || !fs23.existsSync(auth.tokenPath)) {
3136
3901
  return null;
3137
3902
  }
3138
3903
  const fileToken = readGatewayTokenFromPath(auth.tokenPath);
3139
3904
  return fileToken || null;
3140
3905
  }
3141
3906
  function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
3142
- if (auth.tokenPath && fs20.existsSync(auth.tokenPath)) {
3907
+ if (auth.tokenPath && fs23.existsSync(auth.tokenPath)) {
3143
3908
  return auth;
3144
3909
  }
3145
3910
  const token = readGatewayToken(auth);
@@ -3177,7 +3942,7 @@ async function createManagedAppServerAuth(options) {
3177
3942
  options.stateDir,
3178
3943
  options.instanceId
3179
3944
  );
3180
- fs20.mkdirSync(path18.dirname(gatewayLogPath), { recursive: true });
3945
+ fs23.mkdirSync(path20.dirname(gatewayLogPath), { recursive: true });
3181
3946
  rotateLog(gatewayLogPath);
3182
3947
  const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
3183
3948
  const gatewayArgs = [];
@@ -3252,20 +4017,20 @@ function canReuseManagedAppServer(appServer) {
3252
4017
  }
3253
4018
 
3254
4019
  // src/engine/bridge-app-server-lifecycle.ts
3255
- import * as fs21 from "fs";
3256
- import * as path19 from "path";
4020
+ import * as fs24 from "fs";
4021
+ import * as path21 from "path";
3257
4022
  var DEFAULT_APP_SERVER_URL3 = "ws://127.0.0.1:4501";
3258
4023
  var APP_SERVER_START_TIMEOUT_MS = 2e4;
3259
4024
  var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
3260
4025
  function isAppServerUsedByOtherBridge(stateDir, excludeInstanceId, appServer) {
3261
- const pidDir = path19.join(stateDir, "pids");
3262
- if (!fs21.existsSync(pidDir)) return false;
3263
- for (const name of fs21.readdirSync(pidDir)) {
4026
+ const pidDir = path21.join(stateDir, "pids");
4027
+ if (!fs24.existsSync(pidDir)) return false;
4028
+ for (const name of fs24.readdirSync(pidDir)) {
3264
4029
  if (!name.startsWith("bridge-") || !name.endsWith(".json")) continue;
3265
4030
  const otherId = name.slice("bridge-".length, -".json".length);
3266
4031
  if (otherId === excludeInstanceId) continue;
3267
4032
  try {
3268
- const raw = fs21.readFileSync(path19.join(pidDir, name), "utf-8");
4033
+ const raw = fs24.readFileSync(path21.join(pidDir, name), "utf-8");
3269
4034
  const state = JSON.parse(raw);
3270
4035
  if (state.appServer?.url === appServer.url && state.appServer?.pid === appServer.pid && isProcessAlive(state.pid)) {
3271
4036
  return true;
@@ -3277,16 +4042,16 @@ function isAppServerUsedByOtherBridge(stateDir, excludeInstanceId, appServer) {
3277
4042
  return false;
3278
4043
  }
3279
4044
  function findReusableManagedAppServer(stateDir, publicUrl) {
3280
- const pidDir = path19.join(stateDir, "pids");
3281
- if (!fs21.existsSync(pidDir)) {
4045
+ const pidDir = path21.join(stateDir, "pids");
4046
+ if (!fs24.existsSync(pidDir)) {
3282
4047
  return null;
3283
4048
  }
3284
- for (const name of fs21.readdirSync(pidDir)) {
4049
+ for (const name of fs24.readdirSync(pidDir)) {
3285
4050
  if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
3286
4051
  continue;
3287
4052
  }
3288
4053
  try {
3289
- const raw = fs21.readFileSync(path19.join(pidDir, name), "utf-8");
4054
+ const raw = fs24.readFileSync(path21.join(pidDir, name), "utf-8");
3290
4055
  const parsed = JSON.parse(raw);
3291
4056
  if (parsed.appServer?.url !== publicUrl) {
3292
4057
  continue;
@@ -3358,7 +4123,7 @@ Start the app-server manually:
3358
4123
  );
3359
4124
  }
3360
4125
  const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
3361
- fs21.mkdirSync(path19.dirname(logPath), { recursive: true });
4126
+ fs24.mkdirSync(path21.dirname(logPath), { recursive: true });
3362
4127
  rotateLog(logPath);
3363
4128
  if (options.noAuth) {
3364
4129
  const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
@@ -3553,10 +4318,82 @@ function formatCodexAppServerCommand(command, url) {
3553
4318
  }
3554
4319
 
3555
4320
  // src/engine/bridge-startup.ts
3556
- import * as fs22 from "fs";
3557
- import * as path20 from "path";
4321
+ import * as fs25 from "fs";
4322
+ import * as path22 from "path";
3558
4323
  function getBridgeRuntimeStateDir(repoRoot, instanceId) {
3559
- return path20.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
4324
+ const resolved = path22.resolve(
4325
+ path22.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`)
4326
+ );
4327
+ const expectedBase = path22.resolve(repoRoot, ".tmp") + path22.sep;
4328
+ if (!resolved.startsWith(expectedBase)) {
4329
+ throw new Error(
4330
+ `Path traversal blocked: runtime state dir escapes .tmp/ directory`
4331
+ );
4332
+ }
4333
+ return resolved;
4334
+ }
4335
+ var STALE_DIRECT_HEARTBEAT_MS = 5 * 60 * 1e3;
4336
+ function warnHeartbeatCleanup(instanceId, message) {
4337
+ console.warn(
4338
+ `[tap] heartbeat cleanup skipped for ${instanceId}: ${message}`
4339
+ );
4340
+ }
4341
+ function getHeartbeatActivityMs(record) {
4342
+ const timestamp = new Date(record.lastActivity ?? record.timestamp ?? 0).getTime();
4343
+ return Number.isFinite(timestamp) ? timestamp : null;
4344
+ }
4345
+ function isSameInstanceHeartbeat(key, heartbeat, instanceId) {
4346
+ if (heartbeat.instanceId === instanceId) return true;
4347
+ if (heartbeat.connectHash === `instance:${instanceId}`) return true;
4348
+ return key === instanceId || key.replace(/_/g, "-") === instanceId || key.replace(/-/g, "_") === instanceId;
4349
+ }
4350
+ function cleanupStaleSameInstanceHeartbeats(commsDir, instanceId) {
4351
+ const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
4352
+ if (!fs25.existsSync(heartbeatsPath)) return;
4353
+ const lockPath = path22.join(commsDir, ".heartbeats.lock");
4354
+ try {
4355
+ fs25.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
4356
+ } catch {
4357
+ warnHeartbeatCleanup(instanceId, "heartbeat store busy");
4358
+ return;
4359
+ }
4360
+ try {
4361
+ let store = {};
4362
+ try {
4363
+ store = JSON.parse(
4364
+ fs25.readFileSync(heartbeatsPath, "utf-8")
4365
+ );
4366
+ } catch {
4367
+ warnHeartbeatCleanup(instanceId, "heartbeat store unreadable");
4368
+ return;
4369
+ }
4370
+ let changed = false;
4371
+ for (const [key, heartbeat] of Object.entries(store)) {
4372
+ if (!isSameInstanceHeartbeat(key, heartbeat, instanceId)) continue;
4373
+ const status = heartbeat.status ?? "active";
4374
+ const isDeadBridge = heartbeat.source === "bridge-dispatch" && heartbeat.bridgePid != null && !isProcessAlive(heartbeat.bridgePid);
4375
+ const activityMs = getHeartbeatActivityMs(heartbeat);
4376
+ const isStaleDirect = heartbeat.source !== "bridge-dispatch" && activityMs != null && Date.now() - activityMs > STALE_DIRECT_HEARTBEAT_MS;
4377
+ if (status === "signing-off" || isDeadBridge || isStaleDirect) {
4378
+ delete store[key];
4379
+ changed = true;
4380
+ }
4381
+ }
4382
+ if (!changed) return;
4383
+ const tmpPath = `${heartbeatsPath}.tmp.${process.pid}`;
4384
+ fs25.writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
4385
+ fs25.renameSync(tmpPath, heartbeatsPath);
4386
+ } catch (error) {
4387
+ warnHeartbeatCleanup(
4388
+ instanceId,
4389
+ error instanceof Error ? error.message : String(error)
4390
+ );
4391
+ } finally {
4392
+ try {
4393
+ fs25.unlinkSync(lockPath);
4394
+ } catch {
4395
+ }
4396
+ }
3560
4397
  }
3561
4398
  async function startBridge(options) {
3562
4399
  const {
@@ -3584,12 +4421,14 @@ async function startBridge(options) {
3584
4421
  );
3585
4422
  }
3586
4423
  const previousBridgeState = loadBridgeState(stateDir, instanceId);
4424
+ const previousLifecycle = options.previousLifecycle ?? previousBridgeState?.lifecycle ?? null;
3587
4425
  const previousAppServer = previousBridgeState?.appServer ?? null;
3588
4426
  clearBridgeState(stateDir, instanceId);
4427
+ cleanupStaleSameInstanceHeartbeats(commsDir, instanceId);
3589
4428
  const logPath = logFilePath(stateDir, instanceId);
3590
- fs22.mkdirSync(path20.dirname(logPath), { recursive: true });
4429
+ fs25.mkdirSync(path22.dirname(logPath), { recursive: true });
3591
4430
  rotateLog(logPath);
3592
- const repoRoot = options.repoRoot ?? path20.resolve(stateDir, "..");
4431
+ const repoRoot = options.repoRoot ?? path22.resolve(stateDir, "..");
3593
4432
  const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
3594
4433
  const resolved = resolveNodeRuntime(
3595
4434
  options.runtimeCommand ?? "node",
@@ -3600,6 +4439,7 @@ async function startBridge(options) {
3600
4439
  const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
3601
4440
  let appServer = null;
3602
4441
  let bridgeAppServerUrl = effectiveAppServerUrl;
4442
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3603
4443
  if (runtime === "codex" && options.manageAppServer) {
3604
4444
  appServer = await ensureCodexAppServer({
3605
4445
  instanceId,
@@ -3677,9 +4517,18 @@ async function startBridge(options) {
3677
4517
  const state = {
3678
4518
  pid: bridgePid,
3679
4519
  statePath: pidFilePath(stateDir, instanceId),
3680
- lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString(),
4520
+ lastHeartbeat: startedAt,
3681
4521
  appServer,
3682
- runtimeStateDir
4522
+ runtimeStateDir,
4523
+ lifecycle: transitionBridgeLifecycle(
4524
+ previousLifecycle,
4525
+ "initializing",
4526
+ previousLifecycle ? "bridge restart" : "bridge start",
4527
+ {
4528
+ at: startedAt,
4529
+ incrementRestart: previousLifecycle != null
4530
+ }
4531
+ )
3683
4532
  };
3684
4533
  saveBridgeState(stateDir, instanceId, state);
3685
4534
  return state;
@@ -3699,55 +4548,82 @@ async function startBridge(options) {
3699
4548
  }
3700
4549
 
3701
4550
  // src/engine/bridge-orchestrator.ts
3702
- import * as fs23 from "fs";
3703
- import * as path21 from "path";
4551
+ import * as fs26 from "fs";
4552
+ import * as path23 from "path";
3704
4553
  async function stopBridge(options) {
3705
4554
  const { instanceId, stateDir, platform } = options;
3706
4555
  const state = loadBridgeState(stateDir, instanceId);
3707
4556
  if (!state) {
3708
- return false;
4557
+ return {
4558
+ stopped: false,
4559
+ lifecycle: null
4560
+ };
3709
4561
  }
4562
+ const currentLifecycle = state.lifecycle ?? null;
3710
4563
  if (!isProcessAlive(state.pid)) {
3711
4564
  clearBridgeState(stateDir, instanceId);
3712
- return false;
4565
+ return {
4566
+ stopped: false,
4567
+ lifecycle: transitionBridgeLifecycle(
4568
+ currentLifecycle,
4569
+ "crashed",
4570
+ "bridge pid not alive"
4571
+ )
4572
+ };
3713
4573
  }
4574
+ state.lifecycle = transitionBridgeLifecycle(
4575
+ currentLifecycle,
4576
+ "stopping",
4577
+ "bridge stop requested"
4578
+ );
4579
+ saveBridgeState(stateDir, instanceId, state);
3714
4580
  try {
3715
4581
  await terminateProcess(state.pid, platform);
3716
4582
  } catch {
3717
4583
  }
3718
4584
  clearBridgeState(stateDir, instanceId);
3719
- return true;
4585
+ return {
4586
+ stopped: true,
4587
+ lifecycle: transitionBridgeLifecycle(
4588
+ state.lifecycle ?? currentLifecycle,
4589
+ "stopped",
4590
+ "bridge stopped"
4591
+ )
4592
+ };
3720
4593
  }
3721
4594
  async function restartBridge(options) {
3722
4595
  const { instanceId, stateDir, platform } = options;
3723
4596
  const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
3724
4597
  const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
3725
4598
  const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
3726
- const heartbeatPath = path21.join(runtimeStateDir, "heartbeat.json");
3727
- if (fs23.existsSync(heartbeatPath)) {
4599
+ const heartbeatPath = path23.join(runtimeStateDir, "heartbeat.json");
4600
+ if (fs26.existsSync(heartbeatPath)) {
3728
4601
  const startWait = Date.now();
3729
4602
  while (Date.now() - startWait < drainTimeout) {
3730
4603
  try {
3731
- const hb = JSON.parse(fs23.readFileSync(heartbeatPath, "utf-8"));
4604
+ const hb = JSON.parse(fs26.readFileSync(heartbeatPath, "utf-8"));
3732
4605
  if (!hb.activeTurnId) break;
3733
4606
  } catch {
3734
4607
  break;
3735
4608
  }
3736
- await new Promise((resolve14) => setTimeout(resolve14, 1e3));
4609
+ await new Promise((resolve15) => setTimeout(resolve15, 1e3));
3737
4610
  }
3738
4611
  }
3739
4612
  if (options.headless?.enabled && options.commsDir) {
3740
4613
  const agentName = options.agentName ?? instanceId;
3741
- cleanupHeadlessDispatch(path21.join(options.commsDir, "inbox"), agentName);
4614
+ cleanupHeadlessDispatch(path23.join(options.commsDir, "inbox"), agentName);
3742
4615
  }
3743
- await stopBridge({ instanceId, stateDir, platform });
4616
+ const stopResult = await stopBridge({ instanceId, stateDir, platform });
3744
4617
  return startBridge({
3745
4618
  ...options,
3746
- processExistingMessages: true
4619
+ processExistingMessages: true,
4620
+ previousLifecycle: stopResult.lifecycle ?? options.previousLifecycle ?? null
3747
4621
  });
3748
4622
  }
3749
4623
 
3750
4624
  // src/commands/add.ts
4625
+ init_config();
4626
+ init_instance_config();
3751
4627
  var ADD_HELP = `
3752
4628
  Usage:
3753
4629
  tap add <claude|codex|gemini> [options]
@@ -3900,6 +4776,22 @@ async function addCommand(args) {
3900
4776
  agentName: resolvedAgentName
3901
4777
  });
3902
4778
  saveState(repoRoot, updatedState);
4779
+ const { config: cfg } = resolveConfig({}, repoRoot);
4780
+ try {
4781
+ const {
4782
+ loadInstanceConfig: loadInstCfg,
4783
+ updateInstanceConfig: updateInstCfg,
4784
+ saveInstanceConfig: saveInstCfg
4785
+ } = await Promise.resolve().then(() => (init_instance_config(), instance_config_exports));
4786
+ const existing = loadInstCfg(cfg.stateDir, instanceId);
4787
+ if (existing) {
4788
+ const updated = updateInstCfg(existing, {
4789
+ agentName: resolvedAgentName
4790
+ });
4791
+ saveInstCfg(cfg.stateDir, updated);
4792
+ }
4793
+ } catch {
4794
+ }
3903
4795
  return {
3904
4796
  ok: true,
3905
4797
  command: "add",
@@ -4027,6 +4919,7 @@ async function addCommand(args) {
4027
4919
  "Verification had failures. Runtime may need manual configuration."
4028
4920
  );
4029
4921
  }
4922
+ const { config: resolvedCfg } = resolveConfig({}, repoRoot);
4030
4923
  let bridge = null;
4031
4924
  let effectivePort = port;
4032
4925
  if (mode === "app-server") {
@@ -4035,9 +4928,8 @@ async function addCommand(args) {
4035
4928
  logWarn("Bridge script not found. Bridge not started.");
4036
4929
  warnings.push("Bridge script not found. Run bridge manually.");
4037
4930
  } else {
4038
- const { config: resolvedCfg } = resolveConfig({}, repoRoot);
4039
4931
  if (effectivePort == null && runtime === "codex") {
4040
- const currentState = loadState(repoRoot);
4932
+ const currentState = loadState(repoRoot) ?? state;
4041
4933
  effectivePort = await findNextAvailableAppServerPort(
4042
4934
  currentState,
4043
4935
  resolvedCfg.appServerUrl,
@@ -4072,6 +4964,41 @@ async function addCommand(args) {
4072
4964
  }
4073
4965
  }
4074
4966
  }
4967
+ let effectiveAppServerUrl = resolvedCfg.appServerUrl;
4968
+ if (effectivePort != null) {
4969
+ try {
4970
+ const parsed = new URL(resolvedCfg.appServerUrl);
4971
+ parsed.port = String(effectivePort);
4972
+ effectiveAppServerUrl = parsed.toString().replace(/\/$/, "");
4973
+ } catch {
4974
+ }
4975
+ }
4976
+ const permRole = roleArg === "reviewer" ? "reviewer" : headlessFlag ? "implementer" : void 0;
4977
+ const instConfig = createInstanceConfig({
4978
+ instanceId,
4979
+ runtime,
4980
+ agentName: resolvedAgentName,
4981
+ agentId: null,
4982
+ port: effectivePort,
4983
+ appServerUrl: effectiveAppServerUrl,
4984
+ commsDir: ctx.commsDir,
4985
+ stateDir: ctx.stateDir,
4986
+ repoRoot,
4987
+ role: permRole
4988
+ });
4989
+ if (probe.configPath) {
4990
+ try {
4991
+ const { computeFileHash: computeFileHash2 } = await Promise.resolve().then(() => (init_drift_detector(), drift_detector_exports));
4992
+ const runtimeHash = computeFileHash2(probe.configPath);
4993
+ if (runtimeHash) {
4994
+ instConfig.runtimeConfigHash = runtimeHash;
4995
+ instConfig.lastSyncedToRuntime = (/* @__PURE__ */ new Date()).toISOString();
4996
+ }
4997
+ } catch {
4998
+ }
4999
+ }
5000
+ const instConfigPath = saveInstanceConfig(ctx.stateDir, instConfig);
5001
+ logSuccess(`Instance config: ${instConfigPath}`);
4075
5002
  const instanceState = {
4076
5003
  instanceId,
4077
5004
  runtime,
@@ -4089,6 +5016,8 @@ async function addCommand(args) {
4089
5016
  manageAppServer: runtime === "codex",
4090
5017
  noAuth: false,
4091
5018
  headless,
5019
+ configHash: instConfig.configHash,
5020
+ configSourceFile: instConfigPath,
4092
5021
  warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
4093
5022
  };
4094
5023
  const newState = updateInstanceState(state, instanceId, instanceState);
@@ -4123,6 +5052,8 @@ async function addCommand(args) {
4123
5052
  }
4124
5053
 
4125
5054
  // src/commands/status.ts
5055
+ init_config();
5056
+ init_utils();
4126
5057
  var STATUS_HELP = `
4127
5058
  Usage:
4128
5059
  tap status
@@ -4134,30 +5065,71 @@ Examples:
4134
5065
  npx @hua-labs/tap status
4135
5066
  `.trim();
4136
5067
  function resolveStatus(inst, stateDir) {
4137
- if (!inst.installed) return "not installed";
5068
+ if (!inst.installed) {
5069
+ return {
5070
+ status: "not installed",
5071
+ lifecycle: null,
5072
+ session: null
5073
+ };
5074
+ }
4138
5075
  switch (inst.bridgeMode) {
4139
5076
  case "native-push":
4140
5077
  case "polling":
4141
- return inst.lastVerifiedAt ? "active" : "configured";
4142
- case "app-server":
4143
- if (inst.bridge && isBridgeRunning(stateDir, inst.instanceId)) {
4144
- return "active";
4145
- }
5078
+ return {
5079
+ status: inst.lastVerifiedAt ? "active" : "configured",
5080
+ lifecycle: null,
5081
+ session: null
5082
+ };
5083
+ case "app-server": {
4146
5084
  if (inst.bridge) {
4147
- inst.bridge = null;
5085
+ const lifecycle = resolveBridgeLifecycleSnapshot(
5086
+ stateDir,
5087
+ inst.instanceId,
5088
+ inst.bridge
5089
+ );
5090
+ if (lifecycle.status === "bridge-stale") {
5091
+ inst.bridge = null;
5092
+ return {
5093
+ status: inst.lastVerifiedAt ? "configured" : "installed",
5094
+ lifecycle,
5095
+ session: null
5096
+ };
5097
+ }
5098
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(inst.bridge);
5099
+ return {
5100
+ status: "active",
5101
+ lifecycle,
5102
+ session: deriveCodexSessionState({
5103
+ runtimeHeartbeat,
5104
+ runtimeStateDir: inst.bridge.runtimeStateDir ?? null
5105
+ })
5106
+ };
4148
5107
  }
4149
- return inst.lastVerifiedAt ? "configured" : "installed";
5108
+ return {
5109
+ status: inst.lastVerifiedAt ? "configured" : "installed",
5110
+ lifecycle: deriveBridgeLifecycleState({
5111
+ bridgeStatus: "stopped"
5112
+ }),
5113
+ session: deriveCodexSessionState({ runtimeHeartbeat: null })
5114
+ };
5115
+ }
4150
5116
  default:
4151
- return "installed";
5117
+ return {
5118
+ status: "installed",
5119
+ lifecycle: null,
5120
+ session: null
5121
+ };
4152
5122
  }
4153
5123
  }
4154
- function instanceStatusLine(inst, status) {
5124
+ function instanceStatusLine(inst, status, lifecycle, session) {
4155
5125
  const bridgeInfo = inst.bridge ? ` (pid: ${inst.bridge.pid})` : "";
5126
+ const lifecycleStr = lifecycle?.status ?? "-";
5127
+ const sessionStr = session?.status ?? "-";
4156
5128
  const mode = inst.bridgeMode;
4157
5129
  const portStr = inst.port ? ` port:${inst.port}` : "";
4158
5130
  const restart = inst.restartRequired ? " [restart required]" : "";
4159
5131
  const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
4160
- return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
5132
+ return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${lifecycleStr.padEnd(20)} ${sessionStr.padEnd(18)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
4161
5133
  }
4162
5134
  async function statusCommand(args) {
4163
5135
  if (args.includes("--help") || args.includes("-h")) {
@@ -4201,16 +5173,16 @@ async function statusCommand(args) {
4201
5173
  } else {
4202
5174
  log("");
4203
5175
  log(
4204
- `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(14)} ${"Bridge Mode".padEnd(14)} Details`
5176
+ `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(14)} ${"Lifecycle".padEnd(20)} ${"Session".padEnd(18)} ${"Bridge Mode".padEnd(14)} Details`
4205
5177
  );
4206
5178
  log(
4207
- `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(14)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)}`
5179
+ `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)}`
4208
5180
  );
4209
5181
  for (const id of installed) {
4210
5182
  const inst = state.instances[id];
4211
5183
  if (inst) {
4212
- const status = resolveStatus(inst, stateDir);
4213
- log(instanceStatusLine(inst, status));
5184
+ const { status, lifecycle, session } = resolveStatus(inst, stateDir);
5185
+ log(instanceStatusLine(inst, status, lifecycle, session));
4214
5186
  if (inst.warnings.length > 0) {
4215
5187
  for (const w of inst.warnings) {
4216
5188
  logWarn(` ${w}`);
@@ -4218,6 +5190,8 @@ async function statusCommand(args) {
4218
5190
  }
4219
5191
  instances[id] = {
4220
5192
  status,
5193
+ lifecycle,
5194
+ session,
4221
5195
  runtime: inst.runtime,
4222
5196
  bridgeMode: inst.bridgeMode,
4223
5197
  bridge: inst.bridge,
@@ -4249,8 +5223,11 @@ async function statusCommand(args) {
4249
5223
  };
4250
5224
  }
4251
5225
 
5226
+ // src/commands/remove.ts
5227
+ init_utils();
5228
+
4252
5229
  // src/engine/rollback.ts
4253
- import * as fs24 from "fs";
5230
+ import * as fs27 from "fs";
4254
5231
  async function rollbackRuntime(_instanceId, runtimeState) {
4255
5232
  const errors = [];
4256
5233
  const restoredFiles = [];
@@ -4279,7 +5256,7 @@ async function rollbackRuntime(_instanceId, runtimeState) {
4279
5256
  };
4280
5257
  }
4281
5258
  function rollbackArtifact(artifact) {
4282
- if (!fs24.existsSync(artifact.path)) {
5259
+ if (!fs27.existsSync(artifact.path)) {
4283
5260
  return { restored: false, error: `File not found: ${artifact.path}` };
4284
5261
  }
4285
5262
  switch (artifact.kind) {
@@ -4297,7 +5274,7 @@ function rollbackArtifact(artifact) {
4297
5274
  }
4298
5275
  }
4299
5276
  function rollbackJsonPath(artifact) {
4300
- const raw = fs24.readFileSync(artifact.path, "utf-8");
5277
+ const raw = fs27.readFileSync(artifact.path, "utf-8");
4301
5278
  let config;
4302
5279
  try {
4303
5280
  config = JSON.parse(raw);
@@ -4323,18 +5300,18 @@ function rollbackJsonPath(artifact) {
4323
5300
  cleanEmptyParents(config, artifact.selector);
4324
5301
  }
4325
5302
  const tmp = `${artifact.path}.tmp.${process.pid}`;
4326
- fs24.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
4327
- fs24.renameSync(tmp, artifact.path);
5303
+ fs27.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
5304
+ fs27.renameSync(tmp, artifact.path);
4328
5305
  return { restored: true };
4329
5306
  }
4330
5307
  function rollbackTomlTable(artifact) {
4331
- const content = fs24.readFileSync(artifact.path, "utf-8");
5308
+ const content = fs27.readFileSync(artifact.path, "utf-8");
4332
5309
  const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
4333
5310
  if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
4334
5311
  const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
4335
5312
  const tmp2 = `${artifact.path}.tmp.${process.pid}`;
4336
- fs24.writeFileSync(tmp2, nextContent, "utf-8");
4337
- fs24.renameSync(tmp2, artifact.path);
5313
+ fs27.writeFileSync(tmp2, nextContent, "utf-8");
5314
+ fs27.renameSync(tmp2, artifact.path);
4338
5315
  return { restored: true };
4339
5316
  }
4340
5317
  if (!extractTomlTable(content, artifact.selector)) {
@@ -4344,13 +5321,13 @@ function rollbackTomlTable(artifact) {
4344
5321
  };
4345
5322
  }
4346
5323
  const tmp = `${artifact.path}.tmp.${process.pid}`;
4347
- fs24.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
4348
- fs24.renameSync(tmp, artifact.path);
5324
+ fs27.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
5325
+ fs27.renameSync(tmp, artifact.path);
4349
5326
  return { restored: true };
4350
5327
  }
4351
5328
  function rollbackFile(artifact) {
4352
- if (fs24.existsSync(artifact.path)) {
4353
- fs24.unlinkSync(artifact.path);
5329
+ if (fs27.existsSync(artifact.path)) {
5330
+ fs27.unlinkSync(artifact.path);
4354
5331
  return { restored: true };
4355
5332
  }
4356
5333
  return { restored: false, error: `File not found: ${artifact.path}` };
@@ -4475,12 +5452,12 @@ async function removeCommand(args) {
4475
5452
  logHeader(`@hua-labs/tap remove ${instanceId}`);
4476
5453
  if (instance.bridge) {
4477
5454
  const ctx = createAdapterContext(state.commsDir, repoRoot);
4478
- const stopped = await stopBridge({
5455
+ const stopResult = await stopBridge({
4479
5456
  instanceId,
4480
5457
  stateDir: ctx.stateDir,
4481
5458
  platform: ctx.platform
4482
5459
  });
4483
- if (stopped) {
5460
+ if (stopResult.stopped) {
4484
5461
  logSuccess(`Bridge for ${instanceId} stopped`);
4485
5462
  } else {
4486
5463
  log(`No running bridge for ${instanceId}`);
@@ -4522,118 +5499,21 @@ async function removeCommand(args) {
4522
5499
  }
4523
5500
 
4524
5501
  // src/commands/bridge.ts
4525
- import { existsSync as existsSync21, readFileSync as readFileSync17, renameSync as renameSync11, writeFileSync as writeFileSync12 } from "fs";
4526
- import * as path22 from "path";
4527
- function formatAge(seconds) {
4528
- if (seconds < 60) return `${seconds}s ago`;
4529
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
4530
- return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
4531
- }
4532
- var BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS = 10 * 60 * 1e3;
4533
- var BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
4534
- var BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
4535
- function loadBridgeHeartbeatStore(commsDir) {
4536
- const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
4537
- if (!existsSync21(heartbeatsPath)) return {};
4538
- try {
4539
- return JSON.parse(readFileSync17(heartbeatsPath, "utf-8"));
4540
- } catch {
4541
- return null;
4542
- }
4543
- }
4544
- function saveBridgeHeartbeatStore(commsDir, store) {
4545
- const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
4546
- const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
4547
- writeFileSync12(tmp, JSON.stringify(store, null, 2), "utf-8");
4548
- renameSync11(tmp, heartbeatsPath);
4549
- }
4550
- function parseBridgeHeartbeatAgeMs(record, now) {
4551
- const raw = record.lastActivity ?? record.timestamp;
4552
- if (!raw) return Number.POSITIVE_INFINITY;
4553
- const parsed = new Date(raw).getTime();
4554
- if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
4555
- return Math.max(0, now - parsed);
4556
- }
4557
- function resolveBridgeHeartbeatInstanceId(state, heartbeatId) {
4558
- if (state.instances[heartbeatId]) return heartbeatId;
4559
- const hyphenated = heartbeatId.replace(/_/g, "-");
4560
- if (state.instances[hyphenated]) return hyphenated;
4561
- const underscored = heartbeatId.replace(/-/g, "_");
4562
- if (state.instances[underscored]) return underscored;
4563
- return null;
4564
- }
4565
- function pruneStaleHeartbeatsForBridgeUp(state, stateDir, commsDir) {
4566
- const store = loadBridgeHeartbeatStore(commsDir);
4567
- if (store === null) {
4568
- return {
4569
- removed: 0,
4570
- warning: "Auto-clean skipped \u2014 heartbeats.json unreadable"
4571
- };
4572
- }
4573
- const now = Date.now();
4574
- let removed = 0;
4575
- for (const [heartbeatId, heartbeat] of Object.entries(store)) {
4576
- const ageMs = parseBridgeHeartbeatAgeMs(heartbeat, now);
4577
- const instanceId = resolveBridgeHeartbeatInstanceId(state, heartbeatId);
4578
- const instance = instanceId ? state.instances[instanceId] : null;
4579
- const bridgeBacked = instance?.bridgeMode === "app-server";
4580
- const bridgeRunning = bridgeBacked && instanceId ? getBridgeStatus(stateDir, instanceId) === "running" : false;
4581
- const status = heartbeat.status ?? "active";
4582
- const staleByStatus = status === "signing-off" && ageMs >= BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS;
4583
- const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS;
4584
- const staleByAge = !bridgeRunning && ageMs >= BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS;
4585
- if (staleByStatus || staleByDeadBridge || staleByAge) {
4586
- delete store[heartbeatId];
4587
- removed += 1;
4588
- }
4589
- }
4590
- if (removed > 0) {
4591
- saveBridgeHeartbeatStore(commsDir, store);
4592
- }
4593
- return { removed };
4594
- }
4595
- var BRIDGE_HELP = `
4596
- Usage:
4597
- tap bridge <subcommand> [instance] [options]
4598
-
4599
- Subcommands:
4600
- start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
4601
- start --all Start all registered app-server instances
4602
- stop <instance> Stop bridge for an instance
4603
- stop Stop all running bridges
4604
- status Show bridge status for all instances
4605
- status <instance> Show bridge status for a specific instance
4606
- tui <instance> Show the safe Codex TUI attach command for a running bridge
4607
- watch Monitor bridges and auto-restart stuck/stale ones
5502
+ init_utils();
4608
5503
 
4609
- Options:
4610
- --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
4611
- Overrides the stored name from 'tap add' when needed
4612
- --all Start all registered app-server instances
4613
- --busy-mode <steer|wait> How to handle active turns (default: steer)
4614
- --poll-seconds <n> Inbox poll interval (default: 5)
4615
- --reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
4616
- --message-lookback-minutes <n> Process messages from last N minutes (default: 10)
4617
- --thread-id <id> Resume specific thread
4618
- --ephemeral Use ephemeral thread (no persistence)
4619
- --process-existing-messages Process all existing inbox messages
4620
- --no-server Skip app-server auto-start and connect only
4621
- --no-auth Skip auth gateway (app-server listens directly, localhost only)
4622
-
4623
- Port Assignment:
4624
- Ports are auto-assigned from 4501 on first bridge start if not set via --port
4625
- during 'tap add'. Auto-assigned ports are saved to state for future starts.
5504
+ // src/commands/bridge-start.ts
5505
+ import * as path26 from "path";
5506
+ init_instance_config();
5507
+ init_config();
5508
+ init_utils();
4626
5509
 
4627
- Examples:
4628
- npx @hua-labs/tap bridge start codex --agent-name myAgent
4629
- npx @hua-labs/tap bridge start --all
4630
- npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
4631
- npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
4632
- npx @hua-labs/tap bridge stop codex
4633
- npx @hua-labs/tap bridge stop
4634
- npx @hua-labs/tap bridge status
4635
- npx @hua-labs/tap bridge tui codex
4636
- `.trim();
5510
+ // src/commands/bridge-helpers.ts
5511
+ import * as path24 from "path";
5512
+ function formatAge(seconds) {
5513
+ if (seconds < 60) return `${seconds}s ago`;
5514
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
5515
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
5516
+ }
4637
5517
  function formatAppServerState(appServer) {
4638
5518
  const ownership = appServer.managed ? "managed" : "external";
4639
5519
  const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
@@ -4674,7 +5554,7 @@ function formatThreadSummary(threadId, cwd) {
4674
5554
  return cwd ? `${threadId} (${cwd})` : threadId;
4675
5555
  }
4676
5556
  function normalizeComparablePath(value) {
4677
- return path22.resolve(value).replace(/\\/g, "/").toLowerCase();
5557
+ return path24.resolve(value).replace(/\\/g, "/").toLowerCase();
4678
5558
  }
4679
5559
  function sameOptionalPath(left, right) {
4680
5560
  if (!left || !right) {
@@ -4682,6 +5562,16 @@ function sameOptionalPath(left, right) {
4682
5562
  }
4683
5563
  return normalizeComparablePath(left) === normalizeComparablePath(right);
4684
5564
  }
5565
+ function resolveRecoveredAgentName(instanceId, explicitAgentName, repoRoot, stateDir) {
5566
+ return resolveAgentName(instanceId, explicitAgentName, { repoRoot, stateDir }) ?? void 0;
5567
+ }
5568
+ function formatLifecycleTransition(lifecycle) {
5569
+ if (!lifecycle?.lastTransitionAt) {
5570
+ return null;
5571
+ }
5572
+ const reason = lifecycle.lastTransitionReason ? ` (${lifecycle.lastTransitionReason})` : "";
5573
+ return `${lifecycle.lastTransitionAt}${reason}, restarts=${lifecycle.restartCount}`;
5574
+ }
4685
5575
  function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
4686
5576
  const shared = [];
4687
5577
  for (const [id, inst] of Object.entries(state.instances)) {
@@ -4734,6 +5624,75 @@ function transferManagedAppServerOwnership(state, stateDir, recipientId, appServ
4734
5624
  };
4735
5625
  return true;
4736
5626
  }
5627
+
5628
+ // src/commands/bridge-heartbeat.ts
5629
+ import { existsSync as existsSync25, readFileSync as readFileSync21, renameSync as renameSync13, writeFileSync as writeFileSync14 } from "fs";
5630
+ import * as path25 from "path";
5631
+ var BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS = 10 * 60 * 1e3;
5632
+ var BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
5633
+ var BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
5634
+ function loadBridgeHeartbeatStore(commsDir) {
5635
+ const heartbeatsPath = path25.join(commsDir, "heartbeats.json");
5636
+ if (!existsSync25(heartbeatsPath)) return {};
5637
+ try {
5638
+ return JSON.parse(readFileSync21(heartbeatsPath, "utf-8"));
5639
+ } catch {
5640
+ return null;
5641
+ }
5642
+ }
5643
+ function saveBridgeHeartbeatStore(commsDir, store) {
5644
+ const heartbeatsPath = path25.join(commsDir, "heartbeats.json");
5645
+ const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
5646
+ writeFileSync14(tmp, JSON.stringify(store, null, 2), "utf-8");
5647
+ renameSync13(tmp, heartbeatsPath);
5648
+ }
5649
+ function parseBridgeHeartbeatAgeMs(record, now) {
5650
+ const raw = record.lastActivity ?? record.timestamp;
5651
+ if (!raw) return Number.POSITIVE_INFINITY;
5652
+ const parsed = new Date(raw).getTime();
5653
+ if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
5654
+ return Math.max(0, now - parsed);
5655
+ }
5656
+ function resolveBridgeHeartbeatInstanceId(state, heartbeatId) {
5657
+ if (state.instances[heartbeatId]) return heartbeatId;
5658
+ const hyphenated = heartbeatId.replace(/_/g, "-");
5659
+ if (state.instances[hyphenated]) return hyphenated;
5660
+ const underscored = heartbeatId.replace(/-/g, "_");
5661
+ if (state.instances[underscored]) return underscored;
5662
+ return null;
5663
+ }
5664
+ function pruneStaleHeartbeatsForBridgeUp(state, stateDir, commsDir) {
5665
+ const store = loadBridgeHeartbeatStore(commsDir);
5666
+ if (store === null) {
5667
+ return {
5668
+ removed: 0,
5669
+ warning: "Auto-clean skipped \u2014 heartbeats.json unreadable"
5670
+ };
5671
+ }
5672
+ const now = Date.now();
5673
+ let removed = 0;
5674
+ for (const [heartbeatId, heartbeat] of Object.entries(store)) {
5675
+ const ageMs = parseBridgeHeartbeatAgeMs(heartbeat, now);
5676
+ const instanceId = resolveBridgeHeartbeatInstanceId(state, heartbeatId);
5677
+ const instance = instanceId ? state.instances[instanceId] : null;
5678
+ const bridgeBacked = instance?.bridgeMode === "app-server";
5679
+ const bridgeRunning = bridgeBacked && instanceId ? getBridgeStatus(stateDir, instanceId) === "running" : false;
5680
+ const status = heartbeat.status ?? "active";
5681
+ const staleByStatus = status === "signing-off" && ageMs >= BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS;
5682
+ const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS;
5683
+ const staleByAge = !bridgeRunning && ageMs >= BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS;
5684
+ if (staleByStatus || staleByDeadBridge || staleByAge) {
5685
+ delete store[heartbeatId];
5686
+ removed += 1;
5687
+ }
5688
+ }
5689
+ if (removed > 0) {
5690
+ saveBridgeHeartbeatStore(commsDir, store);
5691
+ }
5692
+ return { removed };
5693
+ }
5694
+
5695
+ // src/commands/bridge-start.ts
4737
5696
  async function bridgeStart(identifier, agentName, flags = {}) {
4738
5697
  const repoRoot = findRepoRoot();
4739
5698
  let state = loadState(repoRoot);
@@ -4774,6 +5733,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4774
5733
  }
4775
5734
  const adapter = getAdapter(instance.runtime);
4776
5735
  const mode = adapter.bridgeMode();
5736
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
5737
+ if (instance.runtime === "codex") {
5738
+ const patched = patchCodexApprovalMode();
5739
+ if (patched) {
5740
+ log(`patched approval_mode \u2192 auto in ${patched}`);
5741
+ const instConfig = loadInstanceConfig(ctx.stateDir, instanceId);
5742
+ if (instConfig) {
5743
+ instConfig.runtimeConfigHash = fileHash(patched);
5744
+ instConfig.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5745
+ saveInstanceConfig(ctx.stateDir, instConfig);
5746
+ }
5747
+ }
5748
+ }
4777
5749
  if (mode !== "app-server") {
4778
5750
  return {
4779
5751
  ok: true,
@@ -4786,14 +5758,18 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4786
5758
  data: { bridgeMode: mode }
4787
5759
  };
4788
5760
  }
4789
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
4790
- if (agentName && agentName !== instance.agentName) {
4791
- instance = { ...instance, agentName };
5761
+ const resolvedAgentName = resolveRecoveredAgentName(
5762
+ instanceId,
5763
+ agentName,
5764
+ repoRoot,
5765
+ ctx.stateDir
5766
+ );
5767
+ if ((resolvedAgentName ?? null) !== instance.agentName) {
5768
+ instance = { ...instance, agentName: resolvedAgentName ?? null };
4792
5769
  const updatedState = updateInstanceState(state, instanceId, instance);
4793
5770
  saveState(repoRoot, updatedState);
4794
5771
  state = updatedState;
4795
5772
  }
4796
- const ctx = createAdapterContext(state.commsDir, repoRoot);
4797
5773
  const bridgeScript = adapter.resolveBridgeScript?.(ctx);
4798
5774
  if (!bridgeScript) {
4799
5775
  return {
@@ -4961,7 +5937,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4961
5937
  messageLookbackMinutes,
4962
5938
  threadId,
4963
5939
  ephemeral,
4964
- processExistingMessages
5940
+ processExistingMessages,
5941
+ previousLifecycle: instance.bridgeLifecycle ?? instance.bridge?.lifecycle ?? null
4965
5942
  });
4966
5943
  } finally {
4967
5944
  if (previousWarmup === void 0) {
@@ -4971,7 +5948,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4971
5948
  }
4972
5949
  }
4973
5950
  logSuccess(`Bridge started (PID: ${bridge.pid})`);
4974
- log(`Log: ${path22.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
5951
+ log(`Log: ${path26.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
4975
5952
  if (bridge.appServer) {
4976
5953
  log(`App server: ${formatAppServerState(bridge.appServer)}`);
4977
5954
  if (bridge.appServer.logPath) {
@@ -4990,7 +5967,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4990
5967
  log(`TUI connect: ${bridge.appServer.url}`);
4991
5968
  }
4992
5969
  }
4993
- const updated = { ...instance, bridge, manageAppServer, noAuth };
5970
+ const updated = {
5971
+ ...instance,
5972
+ bridge,
5973
+ bridgeLifecycle: bridge.lifecycle ?? instance.bridgeLifecycle ?? null,
5974
+ manageAppServer,
5975
+ noAuth
5976
+ };
4994
5977
  const newState = updateInstanceState(state, instanceId, updated);
4995
5978
  saveState(repoRoot, newState);
4996
5979
  return {
@@ -5078,14 +6061,19 @@ async function bridgeStartAll(flags = {}) {
5078
6061
  const failed = [];
5079
6062
  for (const instanceId of appServerInstances) {
5080
6063
  const inst = state.instances[instanceId];
5081
- const storedName = inst?.agentName ?? void 0;
6064
+ const storedName = resolveRecoveredAgentName(
6065
+ instanceId,
6066
+ inst?.agentName ?? void 0,
6067
+ repoRoot,
6068
+ ctx.stateDir
6069
+ );
5082
6070
  if (!storedName) {
5083
6071
  const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
5084
6072
  log(msg);
5085
6073
  warnings.push(msg);
5086
6074
  continue;
5087
6075
  }
5088
- const stateDir = path22.join(repoRoot, ".tap-comms");
6076
+ const stateDir = path26.join(repoRoot, ".tap-comms");
5089
6077
  const currentBridgeState = loadBridgeState(stateDir, instanceId);
5090
6078
  const { manageAppServer, noAuth } = inferRestartMode(
5091
6079
  currentBridgeState,
@@ -5125,6 +6113,9 @@ async function bridgeStartAll(flags = {}) {
5125
6113
  data: { started, failed, prunedHeartbeats }
5126
6114
  };
5127
6115
  }
6116
+
6117
+ // src/commands/bridge-stop.ts
6118
+ init_utils();
5128
6119
  async function bridgeStopOne(identifier) {
5129
6120
  const repoRoot = findRepoRoot();
5130
6121
  const state = loadState(repoRoot);
@@ -5159,14 +6150,14 @@ async function bridgeStopOne(identifier) {
5159
6150
  );
5160
6151
  const appServer = bridgeState?.appServer ?? null;
5161
6152
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
5162
- const stopped = await stopBridge({
6153
+ const stopResult = await stopBridge({
5163
6154
  instanceId,
5164
6155
  stateDir: ctx.stateDir,
5165
6156
  platform: ctx.platform
5166
6157
  });
5167
6158
  let appServerStopped = false;
5168
6159
  let appServerTransferredTo = null;
5169
- if (stopped) {
6160
+ if (stopResult.stopped) {
5170
6161
  logSuccess(`Bridge for ${instanceId} stopped`);
5171
6162
  } else {
5172
6163
  log(`No running bridge for ${instanceId}`);
@@ -5210,11 +6201,15 @@ async function bridgeStopOne(identifier) {
5210
6201
  }
5211
6202
  }
5212
6203
  if (instance) {
5213
- const updated = { ...instance, bridge: null };
6204
+ const updated = {
6205
+ ...instance,
6206
+ bridge: null,
6207
+ bridgeLifecycle: stopResult.lifecycle ?? instance.bridgeLifecycle ?? null
6208
+ };
5214
6209
  const newState = updateInstanceState(state, instanceId, updated);
5215
6210
  saveState(repoRoot, newState);
5216
6211
  }
5217
- if (stopped) {
6212
+ if (stopResult.stopped) {
5218
6213
  return {
5219
6214
  ok: true,
5220
6215
  command: "bridge",
@@ -5273,18 +6268,22 @@ async function bridgeStopAll() {
5273
6268
  appServer
5274
6269
  );
5275
6270
  }
5276
- const didStop = await stopBridge({
6271
+ const stopResult = await stopBridge({
5277
6272
  instanceId,
5278
6273
  stateDir: ctx.stateDir,
5279
6274
  platform: ctx.platform
5280
6275
  });
5281
- if (didStop) {
6276
+ if (stopResult.stopped) {
5282
6277
  logSuccess(`Stopped bridge for ${instanceId}`);
5283
6278
  stopped.push(instanceId);
5284
6279
  }
5285
6280
  const instance = state.instances[instanceId];
5286
- if (instance?.bridge) {
5287
- state.instances[instanceId] = { ...instance, bridge: null };
6281
+ if (instance?.bridge || stopResult.lifecycle) {
6282
+ state.instances[instanceId] = {
6283
+ ...instance,
6284
+ bridge: null,
6285
+ bridgeLifecycle: stopResult.lifecycle ?? instance.bridgeLifecycle ?? null
6286
+ };
5288
6287
  stateChanged = true;
5289
6288
  }
5290
6289
  }
@@ -5320,9 +6319,13 @@ async function bridgeStopAll() {
5320
6319
  data: { stopped, stoppedAppServers }
5321
6320
  };
5322
6321
  }
6322
+
6323
+ // src/commands/bridge-watch.ts
6324
+ init_config();
6325
+ init_utils();
5323
6326
  async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5324
6327
  const repoRoot = findRepoRoot();
5325
- const state = loadState(repoRoot);
6328
+ let state = loadState(repoRoot);
5326
6329
  if (!state) {
5327
6330
  return {
5328
6331
  ok: false,
@@ -5342,14 +6345,27 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5342
6345
  );
5343
6346
  const restarted = [];
5344
6347
  const cleaned = [];
6348
+ const initializing = [];
6349
+ const degraded = [];
5345
6350
  const healthy = [];
5346
6351
  const warnings = [];
6352
+ let stateChanged = false;
5347
6353
  for (const instanceId of instanceIds) {
5348
6354
  const inst = state.instances[instanceId];
5349
6355
  if (!inst?.installed || inst.bridgeMode !== "app-server") continue;
5350
6356
  const status = getBridgeStatus(stateDir, instanceId);
5351
6357
  if (status === "stale") {
5352
6358
  log(`${instanceId}: stale (process dead) \u2014 cleaning up`);
6359
+ state.instances[instanceId] = {
6360
+ ...inst,
6361
+ bridge: null,
6362
+ bridgeLifecycle: transitionBridgeLifecycle(
6363
+ inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null,
6364
+ "crashed",
6365
+ "bridge pid not alive"
6366
+ )
6367
+ };
6368
+ stateChanged = true;
5353
6369
  cleaned.push(instanceId);
5354
6370
  continue;
5355
6371
  }
@@ -5357,6 +6373,24 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5357
6373
  log(`${instanceId}: stopped`);
5358
6374
  continue;
5359
6375
  }
6376
+ const lifecycle = resolveBridgeLifecycleSnapshot(
6377
+ stateDir,
6378
+ instanceId,
6379
+ inst.bridge,
6380
+ inst.bridgeLifecycle ?? null
6381
+ );
6382
+ if (lifecycle.status === "initializing") {
6383
+ initializing.push(instanceId);
6384
+ log(`${instanceId}: initializing`);
6385
+ continue;
6386
+ }
6387
+ if (lifecycle.status === "degraded-no-thread") {
6388
+ degraded.push(instanceId);
6389
+ log(
6390
+ `${instanceId}: degraded-no-thread${lifecycle.savedThreadId ? ` (saved thread ${lifecycle.savedThreadId})` : ""}`
6391
+ );
6392
+ continue;
6393
+ }
5360
6394
  if (isTurnStuck(stateDir, instanceId, stuckThresholdSeconds)) {
5361
6395
  const turnInfo = getTurnInfo(stateDir, instanceId, stuckThresholdSeconds);
5362
6396
  const ageStr = turnInfo?.ageSeconds != null ? formatAge(turnInfo.ageSeconds) : "?";
@@ -5380,6 +6414,12 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5380
6414
  const previousWarmup = process.env.TAP_COLD_START_WARMUP;
5381
6415
  process.env.TAP_COLD_START_WARMUP = "true";
5382
6416
  try {
6417
+ const recoveredAgentName = resolveRecoveredAgentName(
6418
+ instanceId,
6419
+ void 0,
6420
+ repoRoot,
6421
+ ctx.stateDir
6422
+ );
5383
6423
  const newBridgeState = await restartBridge({
5384
6424
  instanceId,
5385
6425
  runtime: inst.runtime,
@@ -5387,7 +6427,7 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5387
6427
  commsDir: ctx.commsDir,
5388
6428
  bridgeScript,
5389
6429
  platform: ctx.platform,
5390
- agentName: inst.agentName ?? void 0,
6430
+ agentName: recoveredAgentName,
5391
6431
  runtimeCommand: resolvedCfg.runtimeCommand,
5392
6432
  appServerUrl: resolvedCfg.appServerUrl,
5393
6433
  repoRoot,
@@ -5395,15 +6435,22 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5395
6435
  headless: inst.headless,
5396
6436
  drainTimeoutSeconds: 30,
5397
6437
  manageAppServer,
5398
- noAuth
6438
+ noAuth,
6439
+ previousLifecycle: inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null
5399
6440
  });
5400
- const updatedInst = { ...inst, bridge: newBridgeState };
6441
+ const updatedInst = {
6442
+ ...inst,
6443
+ agentName: recoveredAgentName ?? inst.agentName ?? null,
6444
+ bridge: newBridgeState,
6445
+ bridgeLifecycle: newBridgeState.lifecycle ?? inst.bridgeLifecycle ?? null
6446
+ };
5401
6447
  const updatedState = updateInstanceState(
5402
6448
  state,
5403
6449
  instanceId,
5404
6450
  updatedInst
5405
6451
  );
5406
6452
  saveState(repoRoot, updatedState);
6453
+ state = updatedState;
5407
6454
  restarted.push(instanceId);
5408
6455
  logSuccess(`${instanceId}: restarted`);
5409
6456
  } catch (err) {
@@ -5425,19 +6472,29 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
5425
6472
  const message = [
5426
6473
  restarted.length > 0 ? `Restarted: ${restarted.join(", ")}` : null,
5427
6474
  cleaned.length > 0 ? `Cleaned stale: ${cleaned.join(", ")}` : null,
6475
+ initializing.length > 0 ? `Initializing: ${initializing.join(", ")}` : null,
6476
+ degraded.length > 0 ? `Degraded: ${degraded.join(", ")}` : null,
5428
6477
  healthy.length > 0 ? `Healthy: ${healthy.join(", ")}` : null
5429
6478
  ].filter(Boolean).join(". ") || "No app-server bridges found";
5430
6479
  log("");
5431
6480
  log(message);
6481
+ if (stateChanged) {
6482
+ saveState(repoRoot, state);
6483
+ }
5432
6484
  return {
5433
6485
  ok: true,
5434
6486
  command: "bridge",
5435
6487
  code: restarted.length > 0 ? "TAP_BRIDGE_WATCH_RESTARTED" : "TAP_BRIDGE_WATCH_OK",
5436
6488
  message,
5437
6489
  warnings,
5438
- data: { restarted, cleaned, healthy }
6490
+ data: { restarted, cleaned, initializing, degraded, healthy }
5439
6491
  };
5440
6492
  }
6493
+
6494
+ // src/commands/bridge-status.ts
6495
+ import * as path27 from "path";
6496
+ init_config();
6497
+ init_utils();
5441
6498
  function bridgeStatusAll() {
5442
6499
  const repoRoot = findRepoRoot();
5443
6500
  const state = loadState(repoRoot);
@@ -5457,11 +6514,12 @@ function bridgeStatusAll() {
5457
6514
  const bridges = {};
5458
6515
  logHeader("@hua-labs/tap bridge status");
5459
6516
  log(
5460
- `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
6517
+ `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"Lifecycle".padEnd(20)} ${"Session".padEnd(18)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
5461
6518
  );
5462
6519
  log(
5463
- `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
6520
+ `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
5464
6521
  );
6522
+ let stateChanged = false;
5465
6523
  for (const instanceId of instanceIds) {
5466
6524
  const inst = state.instances[instanceId];
5467
6525
  if (!inst?.installed) continue;
@@ -5471,6 +6529,8 @@ function bridgeStatusAll() {
5471
6529
  );
5472
6530
  bridges[instanceId] = {
5473
6531
  status: "n/a",
6532
+ lifecycle: null,
6533
+ session: null,
5474
6534
  runtime: inst.runtime,
5475
6535
  pid: null,
5476
6536
  port: inst.port,
@@ -5484,18 +6544,40 @@ function bridgeStatusAll() {
5484
6544
  continue;
5485
6545
  }
5486
6546
  const status = getBridgeStatus(stateDir, instanceId);
5487
- const bridgeState = loadBridgeState(stateDir, instanceId);
6547
+ const bridgeState = loadBridgeState(stateDir, instanceId) ?? inst.bridge;
5488
6548
  const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
5489
6549
  const savedThread = loadRuntimeBridgeThreadState(bridgeState);
6550
+ const lifecycle = deriveBridgeLifecycleState({
6551
+ bridgeStatus: status,
6552
+ bridgeState,
6553
+ runtimeHeartbeat,
6554
+ savedThread,
6555
+ persistedLifecycle: inst.bridgeLifecycle ?? bridgeState?.lifecycle ?? null
6556
+ });
6557
+ const session = status === "running" ? deriveCodexSessionState({
6558
+ runtimeHeartbeat,
6559
+ runtimeStateDir: bridgeState?.runtimeStateDir ?? null
6560
+ }) : null;
5490
6561
  const age = getHeartbeatAge(stateDir, instanceId);
6562
+ if (lifecycle.status === "bridge-stale" && inst.bridge) {
6563
+ state.instances[instanceId] = {
6564
+ ...inst,
6565
+ bridge: null,
6566
+ bridgeLifecycle: transitionBridgeLifecycle(
6567
+ inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null,
6568
+ "crashed",
6569
+ "bridge pid not alive"
6570
+ )
6571
+ };
6572
+ stateChanged = true;
6573
+ }
5491
6574
  const pid = bridgeState?.pid ?? null;
5492
6575
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
5493
6576
  const pidStr = pid ? String(pid) : "-";
5494
6577
  const portStr = inst.port ? String(inst.port) : "-";
5495
6578
  const ageStr = age !== null ? formatAge(age) : "-";
5496
- const statusColor = status === "running" ? "running" : status === "stale" ? "stale!" : "stopped";
5497
6579
  log(
5498
- `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
6580
+ `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(10)} ${lifecycle.status.padEnd(20)} ${(session?.status ?? "-").padEnd(18)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
5499
6581
  );
5500
6582
  if (bridgeState?.appServer) {
5501
6583
  log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
@@ -5518,6 +6600,10 @@ function bridgeStatusAll() {
5518
6600
  ` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
5519
6601
  );
5520
6602
  }
6603
+ const transition = formatLifecycleTransition(lifecycle);
6604
+ if (transition) {
6605
+ log(` Transition: ${transition}`);
6606
+ }
5521
6607
  const turnInfo = getTurnInfo(stateDir, instanceId);
5522
6608
  if (turnInfo?.activeTurnId) {
5523
6609
  const ageStr2 = turnInfo.ageSeconds != null ? formatAge(turnInfo.ageSeconds) : "?";
@@ -5533,6 +6619,8 @@ function bridgeStatusAll() {
5533
6619
  }
5534
6620
  bridges[instanceId] = {
5535
6621
  status,
6622
+ lifecycle,
6623
+ session,
5536
6624
  runtime: inst.runtime,
5537
6625
  pid,
5538
6626
  port: inst.port,
@@ -5547,6 +6635,9 @@ function bridgeStatusAll() {
5547
6635
  if (instanceIds.length === 0) {
5548
6636
  log("No instances installed.");
5549
6637
  }
6638
+ if (stateChanged) {
6639
+ saveState(repoRoot, state);
6640
+ }
5550
6641
  log("");
5551
6642
  return {
5552
6643
  ok: true,
@@ -5612,6 +6703,15 @@ function bridgeStatusOne(identifier) {
5612
6703
  warnings: [],
5613
6704
  data: {
5614
6705
  status: "n/a",
6706
+ lifecycle: {
6707
+ presence: "stopped",
6708
+ status: "stopped",
6709
+ summary: "stopped",
6710
+ lastTransitionAt: null,
6711
+ lastTransitionReason: null,
6712
+ restartCount: 0
6713
+ },
6714
+ session: null,
5615
6715
  bridgeMode: inst.bridgeMode,
5616
6716
  pid: null,
5617
6717
  port: inst.port,
@@ -5627,12 +6727,25 @@ function bridgeStatusOne(identifier) {
5627
6727
  const { config: resolvedCfg2 } = resolveConfig({}, repoRoot);
5628
6728
  const stateDir = resolvedCfg2.stateDir;
5629
6729
  const status = getBridgeStatus(stateDir, instanceId);
5630
- const bridgeState = loadBridgeState(stateDir, instanceId);
6730
+ const bridgeState = loadBridgeState(stateDir, instanceId) ?? inst.bridge;
5631
6731
  const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
5632
6732
  const savedThread = loadRuntimeBridgeThreadState(bridgeState);
5633
6733
  const age = getHeartbeatAge(stateDir, instanceId);
5634
6734
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
6735
+ const lifecycle = deriveBridgeLifecycleState({
6736
+ bridgeStatus: status,
6737
+ bridgeState,
6738
+ runtimeHeartbeat,
6739
+ savedThread,
6740
+ persistedLifecycle: inst.bridgeLifecycle ?? bridgeState?.lifecycle ?? null
6741
+ });
6742
+ const session = deriveCodexSessionState({
6743
+ runtimeHeartbeat,
6744
+ runtimeStateDir: bridgeState?.runtimeStateDir ?? null
6745
+ });
5635
6746
  log(`Status: ${status}`);
6747
+ log(`Lifecycle: ${lifecycle.summary}`);
6748
+ log(`Session: ${session.summary}`);
5636
6749
  if (bridgeState) {
5637
6750
  log(`PID: ${bridgeState.pid}`);
5638
6751
  log(
@@ -5649,7 +6762,7 @@ function bridgeStatusOne(identifier) {
5649
6762
  );
5650
6763
  }
5651
6764
  log(
5652
- `Log: ${path22.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
6765
+ `Log: ${path27.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
5653
6766
  );
5654
6767
  if (bridgeState.appServer) {
5655
6768
  log(`App server: ${bridgeState.appServer.url}`);
@@ -5681,6 +6794,10 @@ function bridgeStatusOne(identifier) {
5681
6794
  }
5682
6795
  }
5683
6796
  }
6797
+ const transition = formatLifecycleTransition(lifecycle);
6798
+ if (transition) {
6799
+ log(`Transition: ${transition}`);
6800
+ }
5684
6801
  log("");
5685
6802
  return {
5686
6803
  ok: true,
@@ -5692,6 +6809,23 @@ function bridgeStatusOne(identifier) {
5692
6809
  warnings: [],
5693
6810
  data: {
5694
6811
  status,
6812
+ lifecycle: {
6813
+ presence: lifecycle.presence,
6814
+ status: lifecycle.status,
6815
+ summary: lifecycle.summary,
6816
+ lastTransitionAt: lifecycle.lastTransitionAt,
6817
+ lastTransitionReason: lifecycle.lastTransitionReason,
6818
+ restartCount: lifecycle.restartCount
6819
+ },
6820
+ session: {
6821
+ status: session.status,
6822
+ turnState: session.turnState,
6823
+ summary: session.summary,
6824
+ activeTurnId: session.activeTurnId,
6825
+ idleSince: session.idleSince,
6826
+ lastTurnAt: session.lastTurnAt,
6827
+ lastDispatchAt: session.lastDispatchAt
6828
+ },
5695
6829
  bridgeMode: inst.bridgeMode,
5696
6830
  pid: bridgeState?.pid ?? null,
5697
6831
  port: inst.port,
@@ -5704,6 +6838,10 @@ function bridgeStatusOne(identifier) {
5704
6838
  }
5705
6839
  };
5706
6840
  }
6841
+
6842
+ // src/commands/bridge-tui.ts
6843
+ init_config();
6844
+ init_utils();
5707
6845
  function bridgeTuiOne(identifier) {
5708
6846
  const repoRoot = findRepoRoot();
5709
6847
  const state = loadState(repoRoot);
@@ -5820,7 +6958,11 @@ function bridgeTuiOne(identifier) {
5820
6958
  }
5821
6959
  };
5822
6960
  }
5823
- async function bridgeRestart(identifier, flags) {
6961
+
6962
+ // src/commands/bridge-restart.ts
6963
+ init_config();
6964
+ init_utils();
6965
+ async function bridgeRestart(identifier, flags, explicitAgentName) {
5824
6966
  const repoRoot = findRepoRoot();
5825
6967
  const state = loadState(repoRoot);
5826
6968
  if (!state) {
@@ -5893,6 +7035,12 @@ async function bridgeRestart(identifier, flags) {
5893
7035
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
5894
7036
  log(`Drain timeout: ${drainTimeout}s`);
5895
7037
  try {
7038
+ const resolvedAgentName = resolveRecoveredAgentName(
7039
+ instanceId,
7040
+ explicitAgentName,
7041
+ repoRoot,
7042
+ ctx.stateDir
7043
+ );
5896
7044
  const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
5897
7045
  const { manageAppServer, noAuth } = inferRestartMode(
5898
7046
  currentBridgeState,
@@ -5916,7 +7064,7 @@ async function bridgeRestart(identifier, flags) {
5916
7064
  commsDir: ctx.commsDir,
5917
7065
  bridgeScript,
5918
7066
  platform: ctx.platform,
5919
- agentName: inst.agentName ?? void 0,
7067
+ agentName: resolvedAgentName,
5920
7068
  runtimeCommand: resolvedConfig.runtimeCommand,
5921
7069
  appServerUrl: resolvedConfig.appServerUrl,
5922
7070
  repoRoot,
@@ -5924,7 +7072,8 @@ async function bridgeRestart(identifier, flags) {
5924
7072
  headless: inst.headless,
5925
7073
  drainTimeoutSeconds: drainTimeout,
5926
7074
  manageAppServer,
5927
- noAuth
7075
+ noAuth,
7076
+ previousLifecycle: inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null
5928
7077
  });
5929
7078
  } finally {
5930
7079
  if (previousColdStartWarmup === void 0) {
@@ -5934,7 +7083,14 @@ async function bridgeRestart(identifier, flags) {
5934
7083
  }
5935
7084
  }
5936
7085
  logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
5937
- const updated = { ...inst, bridge, manageAppServer, noAuth };
7086
+ const updated = {
7087
+ ...inst,
7088
+ agentName: resolvedAgentName ?? inst.agentName ?? null,
7089
+ bridge,
7090
+ bridgeLifecycle: bridge.lifecycle ?? inst.bridgeLifecycle ?? null,
7091
+ manageAppServer,
7092
+ noAuth
7093
+ };
5938
7094
  const newState = updateInstanceState(state, instanceId, updated);
5939
7095
  saveState(repoRoot, newState);
5940
7096
  return {
@@ -5960,6 +7116,50 @@ async function bridgeRestart(identifier, flags) {
5960
7116
  };
5961
7117
  }
5962
7118
  }
7119
+
7120
+ // src/commands/bridge.ts
7121
+ var BRIDGE_HELP = `
7122
+ Usage:
7123
+ tap bridge <subcommand> [instance] [options]
7124
+
7125
+ Subcommands:
7126
+ start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
7127
+ start --all Start all registered app-server instances
7128
+ stop <instance> Stop bridge for an instance
7129
+ stop Stop all running bridges
7130
+ status Show bridge status for all instances
7131
+ status <instance> Show bridge status for a specific instance
7132
+ tui <instance> Show the safe Codex TUI attach command for a running bridge
7133
+ watch Monitor bridges and auto-restart stuck/stale ones
7134
+
7135
+ Options:
7136
+ --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
7137
+ Overrides the stored name from 'tap add' when needed
7138
+ --all Start all registered app-server instances
7139
+ --busy-mode <steer|wait> How to handle active turns (default: steer)
7140
+ --poll-seconds <n> Inbox poll interval (default: 5)
7141
+ --reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
7142
+ --message-lookback-minutes <n> Process messages from last N minutes (default: 10)
7143
+ --thread-id <id> Resume specific thread
7144
+ --ephemeral Use ephemeral thread (no persistence)
7145
+ --process-existing-messages Process all existing inbox messages
7146
+ --no-server Skip app-server auto-start and connect only
7147
+ --no-auth Skip auth gateway (app-server listens directly, localhost only)
7148
+
7149
+ Port Assignment:
7150
+ Ports are auto-assigned from 4501 on first bridge start if not set via --port
7151
+ during 'tap add'. Auto-assigned ports are saved to state for future starts.
7152
+
7153
+ Examples:
7154
+ npx @hua-labs/tap bridge start codex --agent-name myAgent
7155
+ npx @hua-labs/tap bridge start --all
7156
+ npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
7157
+ npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
7158
+ npx @hua-labs/tap bridge stop codex
7159
+ npx @hua-labs/tap bridge stop
7160
+ npx @hua-labs/tap bridge status
7161
+ npx @hua-labs/tap bridge tui codex
7162
+ `.trim();
5963
7163
  async function bridgeCommand(args) {
5964
7164
  const { positional, flags } = parseArgs(args);
5965
7165
  const subcommand = positional[0];
@@ -6065,21 +7265,67 @@ async function bridgeCommand(args) {
6065
7265
  }
6066
7266
 
6067
7267
  // src/engine/dashboard.ts
6068
- import * as fs25 from "fs";
6069
- import * as path23 from "path";
7268
+ init_config();
7269
+ import * as fs28 from "fs";
7270
+ import * as path28 from "path";
6070
7271
  import { execSync as execSync4 } from "child_process";
6071
- function collectAgents(commsDir) {
6072
- const heartbeatsPath = path23.join(commsDir, "heartbeats.json");
6073
- if (!fs25.existsSync(heartbeatsPath)) return [];
7272
+ function formatAgentLabel(agentIdOrName, displayName) {
7273
+ const normalizedId = agentIdOrName.trim();
7274
+ const normalizedName = displayName?.trim();
7275
+ if (!normalizedId) {
7276
+ return normalizedName ?? agentIdOrName;
7277
+ }
7278
+ if (!normalizedName || normalizedName === normalizedId) {
7279
+ return normalizedId;
7280
+ }
7281
+ return `${normalizedName} [${normalizedId}]`;
7282
+ }
7283
+ function parseIsoAgeSeconds(value) {
7284
+ if (!value) return null;
7285
+ const timestamp = new Date(value).getTime();
7286
+ if (Number.isNaN(timestamp)) return null;
7287
+ return Math.max(0, Math.floor((Date.now() - timestamp) / 1e3));
7288
+ }
7289
+ function resolveHeartbeatInstanceId(heartbeatId, displayName, state) {
7290
+ if (!state) return null;
7291
+ if (state.instances[heartbeatId]?.installed) return heartbeatId;
7292
+ const hyphenated = heartbeatId.replace(/_/g, "-");
7293
+ if (state.instances[hyphenated]?.installed) return hyphenated;
7294
+ const underscored = heartbeatId.replace(/-/g, "_");
7295
+ if (state.instances[underscored]?.installed) return underscored;
7296
+ if (!displayName) return null;
7297
+ const matches = Object.values(state.instances).filter(
7298
+ (inst) => inst?.installed && inst.agentName === displayName
7299
+ );
7300
+ return matches.length === 1 ? matches[0].instanceId : null;
7301
+ }
7302
+ function collectAgents(commsDir, state, bridges) {
7303
+ const heartbeatsPath = path28.join(commsDir, "heartbeats.json");
7304
+ if (!fs28.existsSync(heartbeatsPath)) return [];
6074
7305
  try {
6075
- const raw = fs25.readFileSync(heartbeatsPath, "utf-8");
7306
+ const raw = fs28.readFileSync(heartbeatsPath, "utf-8");
6076
7307
  const data = JSON.parse(raw);
6077
- return Object.entries(data).map(([name, info]) => ({
6078
- name: info.agent ?? name,
6079
- status: info.status ?? null,
6080
- lastActivity: info.lastActivity ?? info.timestamp ?? null,
6081
- joinedAt: info.joinedAt ?? null
6082
- }));
7308
+ return Object.entries(data).map(([agentId, info]) => {
7309
+ const instanceId = resolveHeartbeatInstanceId(
7310
+ agentId,
7311
+ info.agent ?? null,
7312
+ state
7313
+ );
7314
+ const bridge = instanceId ? bridges.find((candidate) => candidate.instanceId === instanceId) ?? null : null;
7315
+ const presence = bridge?.status === "stale" || bridge?.lifecycle?.status === "bridge-stale" ? "bridge-stale" : bridge?.status === "running" ? "bridge-live" : "mcp-only";
7316
+ const lastActivity = info.lastActivity ?? info.timestamp ?? null;
7317
+ const idleBasis = bridge?.session?.idleSince ?? lastActivity;
7318
+ return {
7319
+ name: formatAgentLabel(agentId, info.agent ?? null),
7320
+ instanceId,
7321
+ presence,
7322
+ lifecycle: bridge?.lifecycle?.status ?? null,
7323
+ status: info.status ?? null,
7324
+ lastActivity,
7325
+ joinedAt: info.joinedAt ?? null,
7326
+ idleSeconds: parseIsoAgeSeconds(idleBasis)
7327
+ };
7328
+ });
6083
7329
  } catch {
6084
7330
  return [];
6085
7331
  }
@@ -6095,12 +7341,21 @@ function collectBridges(repoRoot) {
6095
7341
  if (inst.bridgeMode !== "app-server") continue;
6096
7342
  const instanceId = id;
6097
7343
  const status = getBridgeStatus(stateDir, instanceId);
6098
- const bridgeState = loadBridgeState(stateDir, instanceId);
7344
+ const persistedBridgeState = loadBridgeState(stateDir, instanceId);
7345
+ const bridgeState = persistedBridgeState ?? inst.bridge ?? null;
6099
7346
  const age = getHeartbeatAge(stateDir, instanceId);
7347
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
7348
+ const lifecycle = bridgeState != null ? resolveBridgeLifecycleSnapshot(stateDir, instanceId, bridgeState) : null;
7349
+ const session = bridgeState != null ? deriveCodexSessionState({
7350
+ runtimeHeartbeat,
7351
+ runtimeStateDir: bridgeState.runtimeStateDir ?? null
7352
+ }) : null;
6100
7353
  bridges.push({
6101
7354
  instanceId: id,
6102
7355
  runtime: inst.runtime,
6103
7356
  status,
7357
+ lifecycle,
7358
+ session,
6104
7359
  pid: bridgeState?.pid ?? null,
6105
7360
  port: inst.port ?? null,
6106
7361
  heartbeatAge: age,
@@ -6108,22 +7363,22 @@ function collectBridges(repoRoot) {
6108
7363
  });
6109
7364
  }
6110
7365
  }
6111
- const tmpDir = path23.join(repoRoot, ".tmp");
6112
- if (fs25.existsSync(tmpDir)) {
7366
+ const tmpDir = path28.join(repoRoot, ".tmp");
7367
+ if (fs28.existsSync(tmpDir)) {
6113
7368
  try {
6114
- const dirs = fs25.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
7369
+ const dirs = fs28.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
6115
7370
  for (const dir of dirs) {
6116
- const daemonPath = path23.join(tmpDir, dir, "bridge-daemon.json");
6117
- if (!fs25.existsSync(daemonPath)) continue;
7371
+ const daemonPath = path28.join(tmpDir, dir, "bridge-daemon.json");
7372
+ if (!fs28.existsSync(daemonPath)) continue;
6118
7373
  try {
6119
- const raw = fs25.readFileSync(daemonPath, "utf-8");
7374
+ const raw = fs28.readFileSync(daemonPath, "utf-8");
6120
7375
  const daemon = JSON.parse(raw);
6121
7376
  const alreadyCovered = bridges.some(
6122
7377
  (b) => b.pid === daemon.pid && b.pid !== null
6123
7378
  );
6124
7379
  if (alreadyCovered) continue;
6125
- const agentFile = path23.join(tmpDir, dir, "agent-name.txt");
6126
- const agentName = fs25.existsSync(agentFile) ? fs25.readFileSync(agentFile, "utf-8").trim() : dir;
7380
+ const agentFile = path28.join(tmpDir, dir, "agent-name.txt");
7381
+ const agentName = fs28.existsSync(agentFile) ? fs28.readFileSync(agentFile, "utf-8").trim() : dir;
6127
7382
  const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
6128
7383
  const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
6129
7384
  const port = portMatch ? parseInt(portMatch[1], 10) : null;
@@ -6131,6 +7386,8 @@ function collectBridges(repoRoot) {
6131
7386
  instanceId: agentName,
6132
7387
  runtime: "codex",
6133
7388
  status: running ? "running" : "stale",
7389
+ lifecycle: null,
7390
+ session: null,
6134
7391
  pid: daemon.pid ?? null,
6135
7392
  port,
6136
7393
  heartbeatAge: null,
@@ -6177,6 +7434,12 @@ function collectWarnings(bridges, agents) {
6177
7434
  message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
6178
7435
  });
6179
7436
  }
7437
+ if (bridge.lifecycle?.status === "degraded-no-thread") {
7438
+ warnings.push({
7439
+ level: "warn",
7440
+ message: `Bridge ${bridge.instanceId} is degraded (no active thread)`
7441
+ });
7442
+ }
6180
7443
  }
6181
7444
  if (bridges.length === 0) {
6182
7445
  warnings.push({
@@ -6198,8 +7461,9 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
6198
7461
  repoRoot
6199
7462
  );
6200
7463
  const resolved = config;
6201
- const agents = collectAgents(resolved.commsDir);
7464
+ const state = loadState(resolved.repoRoot);
6202
7465
  const bridges = collectBridges(resolved.repoRoot);
7466
+ const agents = collectAgents(resolved.commsDir, state, bridges);
6203
7467
  const prs = collectPRs();
6204
7468
  const warnings = collectWarnings(bridges, agents);
6205
7469
  return {
@@ -6214,6 +7478,7 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
6214
7478
  }
6215
7479
 
6216
7480
  // src/commands/up.ts
7481
+ init_utils();
6217
7482
  var UP_HELP = `
6218
7483
  Usage:
6219
7484
  tap up [bridge-start options]
@@ -6228,6 +7493,18 @@ Examples:
6228
7493
  npx @hua-labs/tap up --no-auth
6229
7494
  npx @hua-labs/tap up --busy-mode wait
6230
7495
  `.trim();
7496
+ function summarizeLifecycle(snapshot) {
7497
+ const ready = snapshot.bridges.filter(
7498
+ (bridge) => bridge.lifecycle?.status === "ready"
7499
+ ).length;
7500
+ const initializing = snapshot.bridges.filter(
7501
+ (bridge) => bridge.lifecycle?.status === "initializing"
7502
+ ).length;
7503
+ const degraded = snapshot.bridges.filter(
7504
+ (bridge) => bridge.lifecycle?.status === "degraded-no-thread"
7505
+ ).length;
7506
+ return `${ready} ready, ${initializing} initializing, ${degraded} degraded`;
7507
+ }
6231
7508
  async function upCommand(args) {
6232
7509
  if (args.includes("--help") || args.includes("-h")) {
6233
7510
  log(UP_HELP);
@@ -6276,7 +7553,7 @@ async function upCommand(args) {
6276
7553
  ok: true,
6277
7554
  command: "up",
6278
7555
  code: "TAP_UP_OK",
6279
- message: `tap up: ${activeBridges} bridge(s) running`,
7556
+ message: `tap up: ${activeBridges} bridge(s) running (${summarizeLifecycle(snapshot)})`,
6280
7557
  warnings: result.warnings,
6281
7558
  data: {
6282
7559
  ...result.data,
@@ -6286,6 +7563,7 @@ async function upCommand(args) {
6286
7563
  }
6287
7564
 
6288
7565
  // src/commands/down.ts
7566
+ init_utils();
6289
7567
  var DOWN_HELP = `
6290
7568
  Usage:
6291
7569
  tap down
@@ -6335,8 +7613,10 @@ async function downCommand(args) {
6335
7613
  }
6336
7614
 
6337
7615
  // src/commands/serve.ts
6338
- import * as path24 from "path";
7616
+ import * as path29 from "path";
6339
7617
  import { spawn as spawn2 } from "child_process";
7618
+ init_utils();
7619
+ init_config();
6340
7620
  var SERVE_HELP = `
6341
7621
  Usage:
6342
7622
  tap serve [options]
@@ -6369,10 +7649,10 @@ async function serveCommand(args) {
6369
7649
  let commsDir;
6370
7650
  const commsDirIdx = args.indexOf("--comms-dir");
6371
7651
  if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
6372
- commsDir = path24.resolve(normalizeTapPath(args[commsDirIdx + 1]));
7652
+ commsDir = path29.resolve(normalizeTapPath(args[commsDirIdx + 1]));
6373
7653
  }
6374
7654
  if (!commsDir && process.env.TAP_COMMS_DIR) {
6375
- commsDir = path24.resolve(normalizeTapPath(process.env.TAP_COMMS_DIR));
7655
+ commsDir = path29.resolve(normalizeTapPath(process.env.TAP_COMMS_DIR));
6376
7656
  }
6377
7657
  if (!commsDir) {
6378
7658
  const state = loadState(repoRoot);
@@ -6412,9 +7692,9 @@ async function serveCommand(args) {
6412
7692
  TAP_COMMS_DIR: commsDir
6413
7693
  }
6414
7694
  });
6415
- return new Promise((resolve14) => {
7695
+ return new Promise((resolve15) => {
6416
7696
  child.on("error", (err) => {
6417
- resolve14({
7697
+ resolve15({
6418
7698
  ok: false,
6419
7699
  command: "serve",
6420
7700
  code: "TAP_INTERNAL_ERROR",
@@ -6424,7 +7704,7 @@ async function serveCommand(args) {
6424
7704
  });
6425
7705
  });
6426
7706
  child.on("exit", (code) => {
6427
- resolve14({
7707
+ resolve15({
6428
7708
  ok: code === 0,
6429
7709
  command: "serve",
6430
7710
  code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
@@ -6437,8 +7717,10 @@ async function serveCommand(args) {
6437
7717
  }
6438
7718
 
6439
7719
  // src/commands/init-worktree.ts
6440
- import * as fs26 from "fs";
6441
- import * as path25 from "path";
7720
+ init_config();
7721
+ init_utils();
7722
+ import * as fs29 from "fs";
7723
+ import * as path30 from "path";
6442
7724
  import { execSync as execSync5 } from "child_process";
6443
7725
  var INIT_WORKTREE_HELP = `
6444
7726
  Usage:
@@ -6475,7 +7757,7 @@ function run(cmd, opts) {
6475
7757
  }
6476
7758
  }
6477
7759
  function toAbsolute(p) {
6478
- const resolved = path25.resolve(p);
7760
+ const resolved = path30.resolve(p);
6479
7761
  return resolved.replace(/\\/g, "/");
6480
7762
  }
6481
7763
  function probeBun(candidate) {
@@ -6506,18 +7788,18 @@ function findBun() {
6506
7788
  }
6507
7789
  }
6508
7790
  const home = process.env.HOME || process.env.USERPROFILE || "";
6509
- const bunHome = path25.join(
7791
+ const bunHome = path30.join(
6510
7792
  home,
6511
7793
  ".bun",
6512
7794
  "bin",
6513
7795
  process.platform === "win32" ? "bun.exe" : "bun"
6514
7796
  );
6515
- if (fs26.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
7797
+ if (fs29.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
6516
7798
  return null;
6517
7799
  }
6518
7800
  function step1CreateWorktree(opts) {
6519
7801
  log("Step 1/9: Creating worktree...");
6520
- if (fs26.existsSync(opts.worktreePath)) {
7802
+ if (fs29.existsSync(opts.worktreePath)) {
6521
7803
  logWarn(`Directory already exists: ${opts.worktreePath}`);
6522
7804
  try {
6523
7805
  run("git rev-parse --git-dir", { cwd: opts.worktreePath });
@@ -6579,22 +7861,22 @@ function step2MergeMain(opts, warnings) {
6579
7861
  }
6580
7862
  function step3CopyPermissions(opts, warnings) {
6581
7863
  log("Step 3/9: Copying permissions...");
6582
- const srcSettings = path25.join(
7864
+ const srcSettings = path30.join(
6583
7865
  opts.repoRoot,
6584
7866
  ".claude",
6585
7867
  "settings.local.json"
6586
7868
  );
6587
- const destDir = path25.join(opts.worktreePath, ".claude");
6588
- const destSettings = path25.join(destDir, "settings.local.json");
6589
- if (!fs26.existsSync(srcSettings)) {
7869
+ const destDir = path30.join(opts.worktreePath, ".claude");
7870
+ const destSettings = path30.join(destDir, "settings.local.json");
7871
+ if (!fs29.existsSync(srcSettings)) {
6590
7872
  warn(
6591
7873
  warnings,
6592
7874
  "No .claude/settings.local.json found in main repo. Skipping."
6593
7875
  );
6594
7876
  return;
6595
7877
  }
6596
- fs26.mkdirSync(destDir, { recursive: true });
6597
- fs26.copyFileSync(srcSettings, destSettings);
7878
+ fs29.mkdirSync(destDir, { recursive: true });
7879
+ fs29.copyFileSync(srcSettings, destSettings);
6598
7880
  logSuccess("Copied settings.local.json");
6599
7881
  try {
6600
7882
  run("git update-index --skip-worktree .claude/settings.local.json", {
@@ -6619,7 +7901,7 @@ function step4GenerateMcpJson(opts, warnings) {
6619
7901
  const wtAbs = toAbsolute(opts.worktreePath);
6620
7902
  const bunAbs = toAbsolute(bunPath);
6621
7903
  const commsAbs = toAbsolute(opts.commsDir);
6622
- const channelEntry = path25.join(
7904
+ const channelEntry = path30.join(
6623
7905
  wtAbs,
6624
7906
  "packages/tap-plugin/channels/tap-comms.ts"
6625
7907
  );
@@ -6636,8 +7918,8 @@ function step4GenerateMcpJson(opts, warnings) {
6636
7918
  }
6637
7919
  }
6638
7920
  };
6639
- const mcpPath = path25.join(opts.worktreePath, ".mcp.json");
6640
- fs26.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
7921
+ const mcpPath = path30.join(opts.worktreePath, ".mcp.json");
7922
+ fs29.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
6641
7923
  logSuccess(`.mcp.json generated (absolute paths + cwd)`);
6642
7924
  log(` bun: ${bunAbs}`);
6643
7925
  log(` comms: ${commsAbs}`);
@@ -6675,16 +7957,16 @@ function step6BuildEslintPlugin(opts, warnings) {
6675
7957
  }
6676
7958
  function step7VerifyComms(opts, warnings) {
6677
7959
  log("Step 7/9: Verifying comms directory...");
6678
- if (!fs26.existsSync(opts.commsDir)) {
7960
+ if (!fs29.existsSync(opts.commsDir)) {
6679
7961
  warn(warnings, `Comms directory not found: ${opts.commsDir}`);
6680
7962
  warn(warnings, "Create it or run: npx @hua-labs/tap init");
6681
7963
  return;
6682
7964
  }
6683
7965
  const requiredDirs = ["inbox", "findings", "reviews", "letters"];
6684
7966
  for (const dir of requiredDirs) {
6685
- const dirPath = path25.join(opts.commsDir, dir);
6686
- if (!fs26.existsSync(dirPath)) {
6687
- fs26.mkdirSync(dirPath, { recursive: true });
7967
+ const dirPath = path30.join(opts.commsDir, dir);
7968
+ if (!fs29.existsSync(dirPath)) {
7969
+ fs29.mkdirSync(dirPath, { recursive: true });
6688
7970
  logSuccess(`Created ${dir}/`);
6689
7971
  }
6690
7972
  }
@@ -6743,17 +8025,17 @@ async function initWorktreeCommand(args) {
6743
8025
  }
6744
8026
  const repoRoot = findRepoRoot();
6745
8027
  const { config } = resolveConfig({}, repoRoot);
6746
- const branch = typeof flags["branch"] === "string" ? flags["branch"] : path25.basename(path25.resolve(worktreePath));
8028
+ const branch = typeof flags["branch"] === "string" ? flags["branch"] : path30.basename(path30.resolve(worktreePath));
6747
8029
  const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
6748
8030
  const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
6749
8031
  const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
6750
8032
  const skipInstall = flags["skip-install"] === true;
6751
8033
  const opts = {
6752
- worktreePath: path25.resolve(worktreePath),
8034
+ worktreePath: path30.resolve(worktreePath),
6753
8035
  branch,
6754
8036
  base,
6755
8037
  mission,
6756
- commsDir: path25.resolve(commsDir),
8038
+ commsDir: path30.resolve(commsDir),
6757
8039
  skipInstall,
6758
8040
  repoRoot
6759
8041
  };
@@ -6799,6 +8081,7 @@ async function initWorktreeCommand(args) {
6799
8081
  }
6800
8082
 
6801
8083
  // src/commands/dashboard.ts
8084
+ init_utils();
6802
8085
  function formatAge2(seconds) {
6803
8086
  if (seconds === null) return "-";
6804
8087
  if (seconds < 60) return `${seconds}s ago`;
@@ -6836,14 +8119,16 @@ function renderSnapshot(snapshot) {
6836
8119
  if (snapshot.agents.length === 0) {
6837
8120
  log(" (no heartbeats)");
6838
8121
  } else {
8122
+ log(
8123
+ ` ${"Agent".padEnd(18)} ${"Presence".padEnd(18)} ${"Lifecycle".padEnd(20)} ${"Idle"}`
8124
+ );
8125
+ log(
8126
+ ` ${"\u2500".repeat(18)} ${"\u2500".repeat(18)} ${"\u2500".repeat(20)} ${"\u2500".repeat(12)}`
8127
+ );
6839
8128
  for (const agent of snapshot.agents) {
6840
- const activity = agent.lastActivity ? formatAge2(
6841
- Math.floor(
6842
- (Date.now() - new Date(agent.lastActivity).getTime()) / 1e3
6843
- )
6844
- ) : "unknown";
6845
- const status = agent.status ?? "unknown";
6846
- log(` ${agent.name.padEnd(12)} ${status.padEnd(10)} active ${activity}`);
8129
+ log(
8130
+ ` ${truncate(agent.name, 18).padEnd(18)} ${agent.presence.padEnd(18)} ${String(agent.lifecycle ?? "-").padEnd(20)} ${formatAge2(agent.idleSeconds)}`
8131
+ );
6847
8132
  }
6848
8133
  }
6849
8134
  log("");
@@ -6852,15 +8137,16 @@ function renderSnapshot(snapshot) {
6852
8137
  log(" (none)");
6853
8138
  } else {
6854
8139
  log(
6855
- ` ${"Instance".padEnd(20)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Heartbeat"}`
8140
+ ` ${"Instance".padEnd(20)} ${"Status".padEnd(10)} ${"Lifecycle".padEnd(20)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Heartbeat"}`
6856
8141
  );
6857
8142
  log(
6858
- ` ${"\u2500".repeat(20)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(12)}`
8143
+ ` ${"\u2500".repeat(20)} ${"\u2500".repeat(10)} ${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(12)}`
6859
8144
  );
6860
8145
  for (const b of snapshot.bridges) {
6861
8146
  const headlessTag = b.headless ? " [H]" : "";
8147
+ const lifecycle = b.lifecycle?.status ?? "-";
6862
8148
  log(
6863
- ` ${truncate(b.instanceId + headlessTag, 20).padEnd(20)} ${formatStatus(b.status).padEnd(10)} ${(b.pid ? String(b.pid) : "-").padEnd(8)} ${(b.port ? String(b.port) : "-").padEnd(6)} ${formatAge2(b.heartbeatAge)}`
8149
+ ` ${truncate(b.instanceId + headlessTag, 20).padEnd(20)} ${formatStatus(b.status).padEnd(10)} ${truncate(lifecycle, 20).padEnd(20)} ${(b.pid ? String(b.pid) : "-").padEnd(8)} ${(b.port ? String(b.port) : "-").padEnd(6)} ${formatAge2(b.heartbeatAge)}`
6864
8150
  );
6865
8151
  }
6866
8152
  }
@@ -6974,18 +8260,21 @@ async function dashboardCommand(args) {
6974
8260
 
6975
8261
  // src/commands/doctor.ts
6976
8262
  import {
6977
- existsSync as existsSync24,
6978
- mkdirSync as mkdirSync13,
6979
- readdirSync as readdirSync6,
6980
- readFileSync as readFileSync19,
6981
- renameSync as renameSync12,
8263
+ existsSync as existsSync28,
8264
+ mkdirSync as mkdirSync14,
8265
+ readdirSync as readdirSync8,
8266
+ readFileSync as readFileSync23,
8267
+ renameSync as renameSync14,
6982
8268
  statSync as statSync3,
6983
- unlinkSync as unlinkSync6,
6984
- writeFileSync as writeFileSync14
8269
+ unlinkSync as unlinkSync8,
8270
+ writeFileSync as writeFileSync16
6985
8271
  } from "fs";
6986
8272
  import { homedir as homedir3 } from "os";
6987
8273
  import { spawnSync as spawnSync6 } from "child_process";
6988
- import { dirname as dirname15, join as join23, resolve as resolve13 } from "path";
8274
+ import { dirname as dirname15, join as join27, resolve as resolve14 } from "path";
8275
+ init_config();
8276
+ init_drift_detector();
8277
+ init_utils();
6989
8278
  var PASS = "pass";
6990
8279
  var WARN = "warn";
6991
8280
  var FAIL = "fail";
@@ -6999,7 +8288,7 @@ var CODEX_ENV_DRIFT_KEYS = [
6999
8288
  ];
7000
8289
  var CODEX_SESSION_NEUTRAL_NAME = "<set-per-session>";
7001
8290
  function normalizeComparablePath2(value) {
7002
- return resolve13(value).replace(/\\/g, "/").toLowerCase();
8291
+ return resolve14(value).replace(/\\/g, "/").toLowerCase();
7003
8292
  }
7004
8293
  function samePath(left, right) {
7005
8294
  return normalizeComparablePath2(left) === normalizeComparablePath2(right);
@@ -7017,10 +8306,10 @@ function appendWarningMessage(message, extra) {
7017
8306
  return message.includes(extra) ? message : `${message}; ${extra}`;
7018
8307
  }
7019
8308
  function findCodexConfigPath3() {
7020
- return join23(homedir3(), ".codex", "config.toml");
8309
+ return join27(homedir3(), ".codex", "config.toml");
7021
8310
  }
7022
8311
  function canonicalizeTrustPath3(targetPath) {
7023
- let resolved = resolve13(targetPath).replace(/\//g, "\\");
8312
+ let resolved = resolve14(targetPath).replace(/\//g, "\\");
7024
8313
  const driveRoot = /^[A-Za-z]:\\$/;
7025
8314
  if (!driveRoot.test(resolved)) {
7026
8315
  resolved = resolved.replace(/\\+$/g, "");
@@ -7032,10 +8321,10 @@ function trustSelector2(targetPath) {
7032
8321
  }
7033
8322
  function writeTomlAtomically(filePath, content) {
7034
8323
  const dir = dirname15(filePath);
7035
- mkdirSync13(dir, { recursive: true });
8324
+ mkdirSync14(dir, { recursive: true });
7036
8325
  const tmp = `${filePath}.tmp.${process.pid}`;
7037
- writeFileSync14(tmp, content, "utf-8");
7038
- renameSync12(tmp, filePath);
8326
+ writeFileSync16(tmp, content, "utf-8");
8327
+ renameSync14(tmp, filePath);
7039
8328
  }
7040
8329
  function hasInstalledCodexInstance(state) {
7041
8330
  return state ? Object.values(state.instances).some(
@@ -7043,7 +8332,7 @@ function hasInstalledCodexInstance(state) {
7043
8332
  ) : false;
7044
8333
  }
7045
8334
  function getCodexTrustTargets(repoRoot) {
7046
- return [...new Set([repoRoot, process.cwd()].map((value) => resolve13(value)))];
8335
+ return [...new Set([repoRoot, process.cwd()].map((value) => resolve14(value)))];
7047
8336
  }
7048
8337
  function buildSessionNeutralCodexEnv(env) {
7049
8338
  const neutralEnv = {
@@ -7087,7 +8376,7 @@ function repairCodexConfig(repoRoot, commsDir) {
7087
8376
  spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
7088
8377
  );
7089
8378
  }
7090
- const existingContent = existsSync24(spec.configPath) ? readFileSync19(spec.configPath, "utf-8") : "";
8379
+ const existingContent = existsSync28(spec.configPath) ? readFileSync23(spec.configPath, "utf-8") : "";
7091
8380
  const existingTapEnvTable = extractTomlTable(
7092
8381
  existingContent,
7093
8382
  "mcp_servers.tap.env"
@@ -7121,7 +8410,8 @@ function repairCodexConfig(repoRoot, commsDir) {
7121
8410
  "mcp_servers.tap",
7122
8411
  {
7123
8412
  command: spec.managed.command,
7124
- args: spec.managed.args
8413
+ args: spec.managed.args,
8414
+ approval_mode: "auto"
7125
8415
  },
7126
8416
  extractTomlTable(existingContent, "mcp_servers.tap")
7127
8417
  )
@@ -7153,22 +8443,22 @@ function repairCodexConfig(repoRoot, commsDir) {
7153
8443
  return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
7154
8444
  }
7155
8445
  function countFiles(dir, ext = ".md") {
7156
- if (!existsSync24(dir)) return 0;
8446
+ if (!existsSync28(dir)) return 0;
7157
8447
  try {
7158
- return readdirSync6(dir).filter((f) => f.endsWith(ext)).length;
8448
+ return readdirSync8(dir).filter((f) => f.endsWith(ext)).length;
7159
8449
  } catch {
7160
8450
  return 0;
7161
8451
  }
7162
8452
  }
7163
8453
  function recentFileCount(dir, withinMs) {
7164
- if (!existsSync24(dir)) return 0;
8454
+ if (!existsSync28(dir)) return 0;
7165
8455
  const cutoff = Date.now() - withinMs;
7166
8456
  let count = 0;
7167
8457
  try {
7168
- for (const f of readdirSync6(dir)) {
8458
+ for (const f of readdirSync8(dir)) {
7169
8459
  if (!f.endsWith(".md")) continue;
7170
8460
  try {
7171
- if (statSync3(join23(dir, f)).mtimeMs > cutoff) count++;
8461
+ if (statSync3(join27(dir, f)).mtimeMs > cutoff) count++;
7172
8462
  } catch {
7173
8463
  }
7174
8464
  }
@@ -7177,19 +8467,19 @@ function recentFileCount(dir, withinMs) {
7177
8467
  return count;
7178
8468
  }
7179
8469
  function loadDoctorHeartbeatStore(commsDir) {
7180
- const heartbeatsPath = join23(commsDir, "heartbeats.json");
7181
- if (!existsSync24(heartbeatsPath)) return null;
8470
+ const heartbeatsPath = join27(commsDir, "heartbeats.json");
8471
+ if (!existsSync28(heartbeatsPath)) return null;
7182
8472
  try {
7183
- return JSON.parse(readFileSync19(heartbeatsPath, "utf-8"));
8473
+ return JSON.parse(readFileSync23(heartbeatsPath, "utf-8"));
7184
8474
  } catch {
7185
8475
  return null;
7186
8476
  }
7187
8477
  }
7188
8478
  function saveDoctorHeartbeatStore(commsDir, store) {
7189
- const heartbeatsPath = join23(commsDir, "heartbeats.json");
8479
+ const heartbeatsPath = join27(commsDir, "heartbeats.json");
7190
8480
  const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
7191
- writeFileSync14(tmp, JSON.stringify(store, null, 2), "utf-8");
7192
- renameSync12(tmp, heartbeatsPath);
8481
+ writeFileSync16(tmp, JSON.stringify(store, null, 2), "utf-8");
8482
+ renameSync14(tmp, heartbeatsPath);
7193
8483
  }
7194
8484
  function parseHeartbeatAgeMs(record, now) {
7195
8485
  const raw = record.lastActivity ?? record.timestamp;
@@ -7198,7 +8488,7 @@ function parseHeartbeatAgeMs(record, now) {
7198
8488
  if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
7199
8489
  return Math.max(0, now - parsed);
7200
8490
  }
7201
- function resolveHeartbeatInstanceId(state, heartbeatId) {
8491
+ function resolveHeartbeatInstanceId2(state, heartbeatId) {
7202
8492
  if (!state) return null;
7203
8493
  if (state.instances[heartbeatId]) return heartbeatId;
7204
8494
  const hyphenated = heartbeatId.replace(/_/g, "-");
@@ -7214,7 +8504,7 @@ function collectStaleHeartbeatIds(commsDir, state, stateDir) {
7214
8504
  const stale = [];
7215
8505
  for (const [heartbeatId, heartbeat] of Object.entries(store)) {
7216
8506
  const ageMs = parseHeartbeatAgeMs(heartbeat, now);
7217
- const instanceId = resolveHeartbeatInstanceId(state, heartbeatId);
8507
+ const instanceId = resolveHeartbeatInstanceId2(state, heartbeatId);
7218
8508
  const instance = instanceId ? state?.instances[instanceId] : null;
7219
8509
  const bridgeBacked = instance?.bridgeMode === "app-server";
7220
8510
  const bridgeRunning = bridgeBacked && instanceId ? isBridgeRunning(stateDir, instanceId) : false;
@@ -7252,10 +8542,10 @@ function checkComms(commsDir) {
7252
8542
  const checks = [];
7253
8543
  checks.push({
7254
8544
  name: "comms directory",
7255
- status: existsSync24(commsDir) ? PASS : FAIL,
7256
- message: existsSync24(commsDir) ? commsDir : `Not found: ${commsDir}`,
7257
- fix: existsSync24(commsDir) ? void 0 : () => {
7258
- mkdirSync13(commsDir, { recursive: true });
8545
+ status: existsSync28(commsDir) ? PASS : FAIL,
8546
+ message: existsSync28(commsDir) ? commsDir : `Not found: ${commsDir}`,
8547
+ fix: existsSync28(commsDir) ? void 0 : () => {
8548
+ mkdirSync14(commsDir, { recursive: true });
7259
8549
  return `Created ${commsDir}`;
7260
8550
  }
7261
8551
  });
@@ -7264,22 +8554,22 @@ function checkComms(commsDir) {
7264
8554
  ["reviews", false],
7265
8555
  ["findings", false]
7266
8556
  ]) {
7267
- const dir = join23(commsDir, subdir);
7268
- const exists = existsSync24(dir);
8557
+ const dir = join27(commsDir, subdir);
8558
+ const exists = existsSync28(dir);
7269
8559
  checks.push({
7270
8560
  name: `${subdir} directory`,
7271
8561
  status: exists ? PASS : required ? FAIL : WARN,
7272
8562
  message: exists ? subdir === "findings" ? `${countFiles(dir)} findings` : subdir === "inbox" ? `${countFiles(dir)} messages` : "exists" : `Missing${required ? "" : " (optional)"}`,
7273
8563
  fix: exists ? void 0 : () => {
7274
- mkdirSync13(dir, { recursive: true });
8564
+ mkdirSync14(dir, { recursive: true });
7275
8565
  return `Created ${dir}`;
7276
8566
  }
7277
8567
  });
7278
8568
  }
7279
- const heartbeats = join23(commsDir, "heartbeats.json");
7280
- if (existsSync24(heartbeats)) {
8569
+ const heartbeats = join27(commsDir, "heartbeats.json");
8570
+ if (existsSync28(heartbeats)) {
7281
8571
  try {
7282
- const store = JSON.parse(readFileSync19(heartbeats, "utf-8"));
8572
+ const store = JSON.parse(readFileSync23(heartbeats, "utf-8"));
7283
8573
  const agents = Object.keys(store);
7284
8574
  const now = Date.now();
7285
8575
  const active = agents.filter((a) => {
@@ -7393,9 +8683,9 @@ function checkInstances(repoRoot, stateDir, commsDir) {
7393
8683
  }
7394
8684
  }
7395
8685
  }
7396
- const pidPath = join23(stateDir, "pids", `bridge-${id}.json`);
8686
+ const pidPath = join27(stateDir, "pids", `bridge-${id}.json`);
7397
8687
  try {
7398
- unlinkSync6(pidPath);
8688
+ unlinkSync8(pidPath);
7399
8689
  } catch {
7400
8690
  }
7401
8691
  const currentState = loadState(repoRoot);
@@ -7455,8 +8745,8 @@ function checkInstances(repoRoot, stateDir, commsDir) {
7455
8745
  }
7456
8746
  function checkMessageLifecycle(commsDir) {
7457
8747
  const checks = [];
7458
- const inbox = join23(commsDir, "inbox");
7459
- if (!existsSync24(inbox)) {
8748
+ const inbox = join27(commsDir, "inbox");
8749
+ if (!existsSync28(inbox)) {
7460
8750
  checks.push({
7461
8751
  name: "message flow",
7462
8752
  status: FAIL,
@@ -7472,10 +8762,10 @@ function checkMessageLifecycle(commsDir) {
7472
8762
  status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
7473
8763
  message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
7474
8764
  });
7475
- const receiptsPath = join23(commsDir, "receipts", "receipts.json");
7476
- if (existsSync24(receiptsPath)) {
8765
+ const receiptsPath = join27(commsDir, "receipts", "receipts.json");
8766
+ if (existsSync28(receiptsPath)) {
7477
8767
  try {
7478
- const receipts = JSON.parse(readFileSync19(receiptsPath, "utf-8"));
8768
+ const receipts = JSON.parse(readFileSync23(receiptsPath, "utf-8"));
7479
8769
  const receiptCount = Object.keys(receipts).length;
7480
8770
  checks.push({
7481
8771
  name: "read receipts",
@@ -7494,8 +8784,8 @@ function checkMessageLifecycle(commsDir) {
7494
8784
  }
7495
8785
  function checkMcpServer(repoRoot) {
7496
8786
  const checks = [];
7497
- const mcpJson = join23(repoRoot, ".mcp.json");
7498
- if (!existsSync24(mcpJson)) {
8787
+ const mcpJson = join27(repoRoot, ".mcp.json");
8788
+ if (!existsSync28(mcpJson)) {
7499
8789
  checks.push({
7500
8790
  name: "MCP config (.mcp.json)",
7501
8791
  status: WARN,
@@ -7505,7 +8795,7 @@ function checkMcpServer(repoRoot) {
7505
8795
  }
7506
8796
  let config;
7507
8797
  try {
7508
- config = JSON.parse(readFileSync19(mcpJson, "utf-8"));
8798
+ config = JSON.parse(readFileSync23(mcpJson, "utf-8"));
7509
8799
  } catch {
7510
8800
  checks.push({
7511
8801
  name: "MCP config (.mcp.json)",
@@ -7548,7 +8838,7 @@ function checkMcpServer(repoRoot) {
7548
8838
  });
7549
8839
  if (hasTapComms.command) {
7550
8840
  const cmd = hasTapComms.command;
7551
- let cmdAvailable = existsSync24(cmd);
8841
+ let cmdAvailable = existsSync28(cmd);
7552
8842
  if (!cmdAvailable) {
7553
8843
  try {
7554
8844
  const result = spawnSync6(cmd, ["--version"], {
@@ -7570,8 +8860,8 @@ function checkMcpServer(repoRoot) {
7570
8860
  const mcpScript = hasTapComms.args[0];
7571
8861
  checks.push({
7572
8862
  name: "MCP server script",
7573
- status: existsSync24(mcpScript) ? PASS : FAIL,
7574
- message: existsSync24(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
8863
+ status: existsSync28(mcpScript) ? PASS : FAIL,
8864
+ message: existsSync28(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
7575
8865
  });
7576
8866
  if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
7577
8867
  checks.push({
@@ -7604,8 +8894,8 @@ function checkMcpServer(repoRoot) {
7604
8894
  } else {
7605
8895
  checks.push({
7606
8896
  name: "MCP TAP_COMMS_DIR",
7607
- status: existsSync24(envCommsDir) ? PASS : FAIL,
7608
- message: existsSync24(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
8897
+ status: existsSync28(envCommsDir) ? PASS : FAIL,
8898
+ message: existsSync28(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
7609
8899
  });
7610
8900
  }
7611
8901
  checks.push({
@@ -7622,7 +8912,7 @@ function checkCodexConfig(repoRoot, commsDir) {
7622
8912
  }
7623
8913
  const checks = [];
7624
8914
  const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
7625
- if (!existsSync24(spec.configPath)) {
8915
+ if (!existsSync28(spec.configPath)) {
7626
8916
  checks.push({
7627
8917
  name: "MCP config (~/.codex/config.toml)",
7628
8918
  status: WARN,
@@ -7631,7 +8921,7 @@ function checkCodexConfig(repoRoot, commsDir) {
7631
8921
  });
7632
8922
  return checks;
7633
8923
  }
7634
- const content = readFileSync19(spec.configPath, "utf-8");
8924
+ const content = readFileSync23(spec.configPath, "utf-8");
7635
8925
  const tapTable = extractTomlTable(content, "mcp_servers.tap");
7636
8926
  const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
7637
8927
  const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
@@ -7683,6 +8973,14 @@ function checkCodexConfig(repoRoot, commsDir) {
7683
8973
  if (typeof actualAgentId === "string" && actualAgentId.trim()) {
7684
8974
  issues.push(`concrete TAP_AGENT_ID persisted (${actualAgentId})`);
7685
8975
  }
8976
+ if (tapTable) {
8977
+ const actualApprovalMode = selectedMain.approval_mode;
8978
+ if (typeof actualApprovalMode !== "string") {
8979
+ issues.push("approval_mode missing (expected auto)");
8980
+ } else if (actualApprovalMode !== "auto") {
8981
+ issues.push(`approval_mode drift (${actualApprovalMode})`);
8982
+ }
8983
+ }
7686
8984
  for (const trustTarget of spec.trustTargets) {
7687
8985
  const trustTable = extractTomlTable(content, trustSelector2(trustTarget));
7688
8986
  if (!trustTable || !trustTable.includes('trust_level = "trusted"')) {
@@ -7707,8 +9005,8 @@ function checkCodexConfig(repoRoot, commsDir) {
7707
9005
  }
7708
9006
  function checkBridgeTurnHealth(repoRoot) {
7709
9007
  const checks = [];
7710
- const tmpDir = join23(repoRoot, ".tmp");
7711
- if (!existsSync24(tmpDir)) return checks;
9008
+ const tmpDir = join27(repoRoot, ".tmp");
9009
+ if (!existsSync28(tmpDir)) return checks;
7712
9010
  const state = loadState(repoRoot);
7713
9011
  const activeMatchers = /* @__PURE__ */ new Set();
7714
9012
  if (state) {
@@ -7721,7 +9019,7 @@ function checkBridgeTurnHealth(repoRoot) {
7721
9019
  }
7722
9020
  let dirs;
7723
9021
  try {
7724
- dirs = readdirSync6(tmpDir).filter((d) => {
9022
+ dirs = readdirSync8(tmpDir).filter((d) => {
7725
9023
  if (!d.startsWith("codex-app-server-bridge")) return false;
7726
9024
  const suffix = d.replace("codex-app-server-bridge-", "");
7727
9025
  if (activeMatchers.size === 0) return true;
@@ -7734,11 +9032,11 @@ function checkBridgeTurnHealth(repoRoot) {
7734
9032
  return checks;
7735
9033
  }
7736
9034
  for (const dir of dirs) {
7737
- const heartbeatPath = join23(tmpDir, dir, "heartbeat.json");
7738
- if (!existsSync24(heartbeatPath)) continue;
9035
+ const heartbeatPath = join27(tmpDir, dir, "heartbeat.json");
9036
+ if (!existsSync28(heartbeatPath)) continue;
7739
9037
  let heartbeat;
7740
9038
  try {
7741
- heartbeat = JSON.parse(readFileSync19(heartbeatPath, "utf-8"));
9039
+ heartbeat = JSON.parse(readFileSync23(heartbeatPath, "utf-8"));
7742
9040
  } catch {
7743
9041
  checks.push({
7744
9042
  name: `turn: ${dir}`,
@@ -7870,11 +9168,71 @@ async function doctorCommand(args) {
7870
9168
  const state = loadState(repoRoot);
7871
9169
  const commsDir = overrides.commsDir ? config.commsDir : state?.commsDir ?? config.commsDir;
7872
9170
  logHeader(`@hua-labs/tap doctor (v${version})${fixMode ? " --fix" : ""}`);
9171
+ function checkConfigDrift() {
9172
+ let driftResults;
9173
+ try {
9174
+ driftResults = checkAllDrift(config.stateDir, state);
9175
+ } catch (err) {
9176
+ return [
9177
+ {
9178
+ name: "drift:infrastructure",
9179
+ status: "warn",
9180
+ message: `Config drift check failed: ${err instanceof Error ? err.message : String(err)}`
9181
+ }
9182
+ ];
9183
+ }
9184
+ const checks = [];
9185
+ for (const result of driftResults) {
9186
+ for (const dc of result.checks) {
9187
+ const check = {
9188
+ name: `drift:${result.instanceId}:${dc.name}`,
9189
+ status: dc.status === "ok" ? "pass" : dc.autoFixable ? "warn" : "fail",
9190
+ message: dc.details ?? void 0
9191
+ };
9192
+ if (dc.autoFixable && dc.status !== "ok") {
9193
+ check.fix = () => {
9194
+ const {
9195
+ loadInstanceConfig: loadInst,
9196
+ saveInstanceConfig: saveInst
9197
+ } = (init_instance_config(), __toCommonJS(instance_config_exports));
9198
+ const {
9199
+ computeFileHash: hashFile
9200
+ } = (init_drift_detector(), __toCommonJS(drift_detector_exports));
9201
+ const instConfig = loadInst(config.stateDir, result.instanceId);
9202
+ if (!instConfig || !state) {
9203
+ return `Skipped: instance config not found for ${result.instanceId}`;
9204
+ }
9205
+ const inst = state.instances[result.instanceId];
9206
+ if (!inst) {
9207
+ return `Skipped: instance not in state.json for ${result.instanceId}`;
9208
+ }
9209
+ inst.agentName = instConfig.agentName;
9210
+ inst.port = instConfig.port;
9211
+ inst.configHash = instConfig.configHash;
9212
+ inst.configSourceFile = inst.configSourceFile || join27(config.stateDir, "instances", `${result.instanceId}.json`);
9213
+ saveState(repoRoot, state);
9214
+ if (inst.configPath && existsSync28(inst.configPath)) {
9215
+ const currentHash = hashFile(inst.configPath);
9216
+ if (instConfig.runtimeConfigHash !== currentHash) {
9217
+ instConfig.runtimeConfigHash = currentHash;
9218
+ instConfig.lastSyncedToRuntime = (/* @__PURE__ */ new Date()).toISOString();
9219
+ saveInst(config.stateDir, instConfig);
9220
+ }
9221
+ }
9222
+ return `Synced state.json + runtime hash for ${result.instanceId}`;
9223
+ };
9224
+ }
9225
+ checks.push(check);
9226
+ }
9227
+ }
9228
+ return checks;
9229
+ }
7873
9230
  function runAllChecks() {
7874
9231
  const checks = [];
7875
9232
  checks.push(...checkComms(commsDir));
7876
9233
  checks.push(...checkStaleHeartbeats(repoRoot, commsDir, config.stateDir));
7877
9234
  checks.push(...checkInstances(repoRoot, config.stateDir, commsDir));
9235
+ checks.push(...checkConfigDrift());
7878
9236
  checks.push(...checkMessageLifecycle(commsDir));
7879
9237
  checks.push(...checkMcpServer(repoRoot));
7880
9238
  checks.push(...checkCodexConfig(repoRoot, commsDir));
@@ -7885,6 +9243,7 @@ async function doctorCommand(args) {
7885
9243
  for (const section of [
7886
9244
  "Comms",
7887
9245
  "Instances",
9246
+ "Config Drift",
7888
9247
  "Messages",
7889
9248
  "MCP",
7890
9249
  "Turns"
@@ -7903,6 +9262,7 @@ async function doctorCommand(args) {
7903
9262
  Instances: initialChecks.filter(
7904
9263
  (c) => c.name.startsWith("bridge:") || c.name.startsWith("instance:") || c.name === "tap state"
7905
9264
  ),
9265
+ "Config Drift": initialChecks.filter((c) => c.name.startsWith("drift:")),
7906
9266
  Messages: initialChecks.filter(
7907
9267
  (c) => ["message flow", "read receipts"].includes(c.name)
7908
9268
  ),
@@ -7970,9 +9330,10 @@ async function doctorCommand(args) {
7970
9330
  }
7971
9331
 
7972
9332
  // src/commands/comms.ts
9333
+ init_utils();
7973
9334
  import { execSync as execSync6, spawnSync as spawnSync7 } from "child_process";
7974
- import * as fs27 from "fs";
7975
- import * as path26 from "path";
9335
+ import * as fs30 from "fs";
9336
+ import * as path31 from "path";
7976
9337
  var COMMS_HELP = `
7977
9338
  Usage:
7978
9339
  tap comms <subcommand>
@@ -7986,7 +9347,7 @@ Examples:
7986
9347
  npx @hua-labs/tap comms push
7987
9348
  `.trim();
7988
9349
  function isGitRepo(dir) {
7989
- return fs27.existsSync(path26.join(dir, ".git"));
9350
+ return fs30.existsSync(path31.join(dir, ".git"));
7990
9351
  }
7991
9352
  function commsPull(commsDir) {
7992
9353
  logHeader("tap comms pull");
@@ -8134,6 +9495,7 @@ async function commsCommand(args) {
8134
9495
  }
8135
9496
 
8136
9497
  // src/commands/watch.ts
9498
+ init_utils();
8137
9499
  var WATCH_HELP = `
8138
9500
  Usage:
8139
9501
  tap watch [options]
@@ -8155,7 +9517,7 @@ Examples:
8155
9517
  npx @hua-labs/tap watch --stuck-threshold 120 # 2 min threshold
8156
9518
  `.trim();
8157
9519
  function delay2(ms) {
8158
- return new Promise((resolve14) => setTimeout(resolve14, ms));
9520
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
8159
9521
  }
8160
9522
  async function watchCommand(args) {
8161
9523
  const { flags } = parseArgs(args);
@@ -8244,8 +9606,8 @@ async function watchCommand(args) {
8244
9606
  import * as http from "http";
8245
9607
 
8246
9608
  // src/engine/missions.ts
8247
- import * as fs28 from "fs";
8248
- import * as path27 from "path";
9609
+ import * as fs31 from "fs";
9610
+ import * as path32 from "path";
8249
9611
  function parseStatus(raw) {
8250
9612
  const trimmed = raw.trim();
8251
9613
  if (trimmed.includes("active")) return "active";
@@ -8271,10 +9633,10 @@ function parseRow(line) {
8271
9633
  return { id: id.toUpperCase(), title, branch, status, owner };
8272
9634
  }
8273
9635
  function parseMissionsFile(repoRoot) {
8274
- const missionsPath = path27.join(repoRoot, "docs", "missions", "MISSIONS.md");
9636
+ const missionsPath = path32.join(repoRoot, "docs", "missions", "MISSIONS.md");
8275
9637
  let content;
8276
9638
  try {
8277
- content = fs28.readFileSync(missionsPath, "utf-8");
9639
+ content = fs31.readFileSync(missionsPath, "utf-8");
8278
9640
  } catch {
8279
9641
  return [];
8280
9642
  }
@@ -8348,6 +9710,8 @@ function fetchPrs(repoRoot) {
8348
9710
  }
8349
9711
 
8350
9712
  // src/commands/gui.ts
9713
+ init_config();
9714
+ init_utils();
8351
9715
  var GUI_HELP = `
8352
9716
  Usage:
8353
9717
  tap gui [options]
@@ -8369,7 +9733,7 @@ function esc(str) {
8369
9733
  }
8370
9734
  function buildHtml(snapshot, turnData) {
8371
9735
  const agentRows = snapshot.agents.map(
8372
- (a) => `<tr><td>${esc(a.name)}</td><td class="${a.status === "active" ? "ok" : "warn"}">${esc(a.status)}</td><td>${a.lastActivity ? esc(new Date(a.lastActivity).toLocaleTimeString()) : "-"}</td></tr>`
9736
+ (a) => `<tr><td>${esc(a.name)}</td><td class="${a.presence === "bridge-live" ? "ok" : a.presence === "bridge-stale" ? "warn" : "off"}">${esc(a.presence)}</td><td>${esc(a.lifecycle ?? "-")}</td><td>${a.lastActivity ? esc(new Date(a.lastActivity).toLocaleTimeString()) : "-"}</td></tr>`
8373
9737
  ).join("\n");
8374
9738
  const bridgeRows = snapshot.bridges.map((b) => {
8375
9739
  const turn = turnData[b.instanceId];
@@ -8407,8 +9771,8 @@ function buildHtml(snapshot, turnData) {
8407
9771
 
8408
9772
  <h2>Agents</h2>
8409
9773
  <table>
8410
- <tr><th>Name</th><th>Status</th><th>Last Activity</th></tr>
8411
- ${agentRows || '<tr><td colspan="3" class="off">No agents</td></tr>'}
9774
+ <tr><th>Name</th><th>Presence</th><th>Lifecycle</th><th>Last Activity</th></tr>
9775
+ ${agentRows || '<tr><td colspan="4" class="off">No agents</td></tr>'}
8412
9776
  </table>
8413
9777
 
8414
9778
  <h2>Bridges</h2>
@@ -8664,10 +10028,10 @@ async function guiCommand(args) {
8664
10028
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
8665
10029
  res.end(buildHtml(snapshot, turnData));
8666
10030
  });
8667
- return new Promise((resolve14) => {
10031
+ return new Promise((resolve15) => {
8668
10032
  server.on("error", (err) => {
8669
10033
  if (err.code === "EADDRINUSE") {
8670
- resolve14({
10034
+ resolve15({
8671
10035
  ok: false,
8672
10036
  command: "gui",
8673
10037
  code: "TAP_PORT_IN_USE",
@@ -8676,7 +10040,7 @@ async function guiCommand(args) {
8676
10040
  data: {}
8677
10041
  });
8678
10042
  } else {
8679
- resolve14({
10043
+ resolve15({
8680
10044
  ok: false,
8681
10045
  command: "gui",
8682
10046
  code: "TAP_GUI_ERROR",
@@ -8696,6 +10060,7 @@ async function guiCommand(args) {
8696
10060
  }
8697
10061
 
8698
10062
  // src/output.ts
10063
+ init_utils();
8699
10064
  function emitResult(result, jsonMode) {
8700
10065
  if (jsonMode) {
8701
10066
  console.log(JSON.stringify(result, null, 2));
@@ -8724,6 +10089,9 @@ function extractJsonFlag(args) {
8724
10089
  return { jsonMode, cleanArgs };
8725
10090
  }
8726
10091
 
10092
+ // src/cli.ts
10093
+ init_utils();
10094
+
8727
10095
  // src/cli-suggest.ts
8728
10096
  var COMMANDS = [
8729
10097
  "init",