triflux 10.49.0 → 10.50.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.
Files changed (122) hide show
  1. package/CLAUDE.md +1 -2
  2. package/README.ko.md +4 -7
  3. package/README.md +4 -7
  4. package/adapters/codex/skills/tfx-live/SKILL.md +129 -0
  5. package/bin/tfx-live.mjs +446 -21
  6. package/bin/triflux.mjs +181 -1507
  7. package/config/mcp-registry.json +10 -8
  8. package/hooks/agy-session-hook.mjs +1 -1
  9. package/hooks/claude-cwd-projection-refresh.mjs +7 -79
  10. package/hooks/codex-session-hook.mjs +3 -8
  11. package/hooks/hook-orchestrator.mjs +8 -648
  12. package/hooks/lib/resolve-root.mjs +1 -1
  13. package/hub/bridge.mjs +290 -51
  14. package/hub/team/claude-daemon-control.mjs +54 -6
  15. package/hub/team/claude-transcript.mjs +240 -0
  16. package/hub/team/cli/commands/start/index.mjs +17 -3
  17. package/hub/team/cli/commands/start/parse-args.mjs +1 -0
  18. package/hub/team/session-context.mjs +203 -0
  19. package/hub/team/uds-orchestrator.mjs +63 -1
  20. package/hub/team/worktree-lifecycle.mjs +0 -1
  21. package/hud/constants.mjs +10 -0
  22. package/hud/hud-qos-status.mjs +23 -4
  23. package/hud/providers/gemini.mjs +4 -1
  24. package/hud/renderers.mjs +10 -6
  25. package/package.json +5 -2
  26. package/references/codex-plugin-cc-analysis.md +5 -10
  27. package/references/codex-plugin-cc-code-patterns.md +3 -3
  28. package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +4 -19
  29. package/scripts/__tests__/mcp-guard-engine-proactive-sync.test.mjs +8 -34
  30. package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +24 -9
  31. package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +0 -2
  32. package/scripts/__tests__/mcp-guard-engine.test.mjs +1 -58
  33. package/scripts/__tests__/setup-cleanup-stale-skills.test.mjs +68 -212
  34. package/scripts/__tests__/skill-surface.test.mjs +2 -53
  35. package/scripts/__tests__/smoke.test.mjs +0 -8
  36. package/scripts/completions/tfx.bash +1 -7
  37. package/scripts/completions/tfx.fish +1 -4
  38. package/scripts/completions/tfx.zsh +1 -10
  39. package/scripts/config-audit.mjs +0 -55
  40. package/scripts/doctor-diagnose.mjs +1 -1
  41. package/scripts/install-commit-hook.mjs +56 -0
  42. package/scripts/lib/doctor-env-checks.mjs +0 -85
  43. package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
  44. package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
  45. package/scripts/lib/mcp-guard-engine.mjs +44 -271
  46. package/scripts/lib/mcp-manifest.mjs +2 -2
  47. package/scripts/lib/session-presence.mjs +249 -0
  48. package/scripts/pack.mjs +4 -0
  49. package/scripts/preflight-cache.mjs +19 -6
  50. package/scripts/release/check-packages-mirror.mjs +8 -0
  51. package/scripts/remote-spawn.mjs +56 -1
  52. package/scripts/run.cjs +1 -1
  53. package/scripts/session-stale-cleanup.mjs +2 -3
  54. package/scripts/setup.mjs +320 -713
  55. package/scripts/sync-hub-mcp-settings.mjs +0 -280
  56. package/scripts/tfx-route.sh +8 -0
  57. package/skills/tfx-doctor/SKILL.md +14 -199
  58. package/skills/tfx-live/SKILL.md +73 -206
  59. package/skills/tfx-remote/SKILL.md +20 -10
  60. package/skills/tfx-research/SKILL.md +5 -2
  61. package/skills/tfx-setup/SKILL.md +14 -574
  62. package/skills/tfx-setup/skill.json +2 -2
  63. package/skills/tfx-wt/SKILL.md +6 -8
  64. package/skills/tfx-wt/skill.json +1 -1
  65. package/tui/setup.mjs +0 -130
  66. package/hooks/agent-route-guard.mjs +0 -113
  67. package/hooks/cross-review-tracker.mjs +0 -135
  68. package/hooks/cto-north-star-brief.mjs +0 -133
  69. package/hooks/error-context.mjs +0 -185
  70. package/hooks/hook-adaptive-collector.mjs +0 -88
  71. package/hooks/hook-manager.mjs +0 -426
  72. package/hooks/hook-registry.json +0 -367
  73. package/hooks/hooks.json +0 -149
  74. package/hooks/keyword-rules.json +0 -676
  75. package/hooks/mcp-config-watcher.mjs +0 -84
  76. package/hooks/permission-safe-allow.mjs +0 -267
  77. package/hooks/pipeline-stop.mjs +0 -435
  78. package/hooks/post-tool-tips.mjs +0 -306
  79. package/hooks/pre-compact-snapshot.mjs +0 -181
  80. package/hooks/safety-guard.mjs +0 -466
  81. package/hooks/session-end-cleanup.mjs +0 -360
  82. package/hooks/session-start-fast.mjs +0 -579
  83. package/hooks/session-start-lake.mjs +0 -81
  84. package/hooks/subagent-tracker.mjs +0 -281
  85. package/hooks/subagent-verifier.mjs +0 -85
  86. package/scripts/__tests__/install-mcp-gateway-startup.test.mjs +0 -367
  87. package/scripts/__tests__/keyword-detector.test.mjs +0 -368
  88. package/scripts/__tests__/mcp-gateway-health-check.test.mjs +0 -261
  89. package/scripts/__tests__/mcp-gateway-wrapper-check.test.mjs +0 -54
  90. package/scripts/__tests__/mcp-guard-engine-watch-http-headers.test.mjs +0 -137
  91. package/scripts/__tests__/session-start-fast.test.mjs +0 -36
  92. package/scripts/codex-gateway-preflight.mjs +0 -208
  93. package/scripts/codex-mcp-gateway-sync.mjs +0 -275
  94. package/scripts/cross-review-gate.mjs +0 -162
  95. package/scripts/cross-review-tracker.mjs +0 -251
  96. package/scripts/install-mcp-gateway-startup.mjs +0 -503
  97. package/scripts/keyword-detector.mjs +0 -485
  98. package/scripts/keyword-rules-expander.mjs +0 -538
  99. package/scripts/lib/cross-review-utils.mjs +0 -59
  100. package/scripts/lib/hook-utils.mjs +0 -16
  101. package/scripts/lib/keyword-rules.mjs +0 -296
  102. package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
  103. package/scripts/lib/mcp-gateway-servers.mjs +0 -70
  104. package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
  105. package/scripts/mcp-gateway-config.mjs +0 -222
  106. package/scripts/mcp-gateway-ensure.mjs +0 -135
  107. package/scripts/mcp-gateway-integration-test.mjs +0 -262
  108. package/scripts/mcp-gateway-start.mjs +0 -304
  109. package/scripts/mcp-gateway-start.ps1 +0 -140
  110. package/scripts/mcp-gateway-verify.mjs +0 -79
  111. package/scripts/mcp-safety-guard.mjs +0 -97
  112. package/scripts/tfx-gate-activate.mjs +0 -98
  113. package/skills/merge-worktree/SKILL.md +0 -144
  114. package/skills/merge-worktree/skill.json +0 -5
  115. package/skills/star-prompt/SKILL.md +0 -220
  116. package/skills/star-prompt/skill.json +0 -4
  117. package/skills/tfx-interview/SKILL.md +0 -371
  118. package/skills/tfx-interview/skill.json +0 -6
  119. package/skills/tfx-plan/SKILL.md +0 -197
  120. package/skills/tfx-plan/skill.json +0 -6
  121. package/skills/tfx-profile/SKILL.md +0 -249
  122. package/skills/tfx-profile/skill.json +0 -5
package/bin/tfx-live.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
+ import { randomBytes } from "node:crypto";
3
4
  import { mkdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
4
5
  import {
5
6
  mkdir,
@@ -21,6 +22,10 @@ import {
21
22
  registryDir,
22
23
  } from "../hub/lib/codex-session-registry.mjs";
23
24
  import { resolveHardCeilingMs } from "../hub/lib/worker-lifecycle.mjs";
25
+ import {
26
+ findClaudeTranscript,
27
+ readClaudeTranscript,
28
+ } from "../hub/team/claude-transcript.mjs";
24
29
  import { createNotifier } from "../hub/team/notify.mjs";
25
30
  import {
26
31
  escapePwshSingleQuoted as escapeRemotePwshSingleQuoted,
@@ -28,6 +33,12 @@ import {
28
33
  shellQuote as remoteShellQuote,
29
34
  validateHost as validateRemoteHost,
30
35
  } from "../hub/team/remote-session.mjs";
36
+ import {
37
+ CONTEXT_THRESHOLDS,
38
+ contextGuard,
39
+ modelContext,
40
+ readCodexContext,
41
+ } from "../hub/team/session-context.mjs";
31
42
 
32
43
  const execFileAsync = promisify(execFile);
33
44
 
@@ -51,7 +62,13 @@ const BRIDGE_TIMEOUT_BUFFER_MS = 15_000;
51
62
  // spill the JSON to a temp file and pass --payload-file instead.
52
63
  const PAYLOAD_FILE_THRESHOLD = 96 * 1024;
53
64
  const VALID_TRANSPORTS = ["tmux", "uds", "auto"];
54
- const BOOLEAN_FLAGS = new Set(["json", "attach-a", "attach-b"]);
65
+ const BOOLEAN_FLAGS = new Set([
66
+ "json",
67
+ "attach-a",
68
+ "attach-b",
69
+ "no-wait",
70
+ "no-relay-tag",
71
+ ]);
55
72
  const PEER_HOP_DONE_MARKER = "<<<TFX_PEER_HOP_DONE>>>";
56
73
  // uds-fallback diagnostics land here, written async so a failed daemon attach
57
74
  // never blocks the tmux fallback path (see writeUdsBugReport / doAskAuto).
@@ -59,16 +76,21 @@ const BUG_REPORT_DIR =
59
76
  process.env.TFX_LIVE_BUG_REPORT_DIR ??
60
77
  pathJoin(homedir(), ".claude", "cache", "triflux", "tfx-live", "bug-reports");
61
78
 
62
- function usage() {
63
- return [
79
+ function usage(command) {
80
+ const lines = [
64
81
  "Usage:",
65
- " tfx-live start --session NAME [--cli codex|claude] [--model ID] [--effort TIER] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
82
+ " tfx-live start --session NAME [--name NAME] [--cli codex|claude] [--model ID] [--effort TIER] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
66
83
  " tfx-live ask --session NAME[:WINDOW.PANE] --prompt TEXT [--cli codex|claude] [--if-busy wait|fail|interrupt] [--busy-timeout 60] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
67
84
  " tfx-live ask --cli codex --transport uds --thread ID|auto --prompt TEXT [--codex-socket PATH|default] [--cwd DIR] [--if-busy wait|fail|steer] [--busy-timeout 60] [--timeout 60] [--max-turn SECONDS]",
68
85
  " tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--config-dir DIR] [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
86
+ " ask options: --no-wait --no-relay-tag --warn-context-pct N --max-context-pct N (0 disables; Claude 60/90, Codex 15/22).",
87
+ " tfx-live compact --cli claude --session NAME [--instructions TEXT] [--if-busy fail|wait] [--timeout 60]",
88
+ " tfx-live wait --cli claude (--short SHORT | --session-id ID) [--request-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 60] [--poll-interval 1500]",
89
+ " tfx-live rename --cli codex --transport uds --thread ID --name NAME [--codex-socket PATH|default]",
69
90
  " transport: auto is the default for Claude when --short/--session-id is present; otherwise tmux. bridge path: --bridge > $TFX_BRIDGE > $TFX_REPO_ROOT/hub/bridge.mjs > bundled Triflux hub/bridge.mjs.",
70
91
  " tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 5]",
71
92
  " tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
93
+ " tfx-live stop --cli claude (--short SHORT | --session-id ID) [--config-dir DIR]",
72
94
  " tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
73
95
  " tfx-live list-sessions --cli codex|claude [--transport tmux|uds] [--codex-socket PATH|default] [--cwd DIR] [--remote HOST (codex tmux only)]",
74
96
  " Claude list-sessions supports local tmux only; --transport uds requires --cli codex.",
@@ -80,10 +102,22 @@ function usage() {
80
102
  " Runs the Claude(UDS)+Codex orchestration engine. --codex-transport app-server-uds drives a real `codex app-server` over WebSocket-over-UDS (experimental); default exec keeps the codex stdio one-shot path.",
81
103
  " tfx-live cto-hygiene-notify --root DIR --state-file PATH [--json]",
82
104
  " One-shot CTO hygiene dry-run notification: sends only when actionable hygiene state changes; no polling or apply/steward lock.",
83
- ].join("\n");
105
+ ];
106
+ if (!command) return lines.join("\n");
107
+ const selected = lines.filter(
108
+ (line) =>
109
+ line.startsWith(` tfx-live ${command} `) ||
110
+ (command === "ask" && line.startsWith(" ask options:")),
111
+ );
112
+ return selected.length
113
+ ? ["Usage:", ...selected].join("\n")
114
+ : lines.join("\n");
84
115
  }
85
116
 
86
117
  function parseCli(argv) {
118
+ if (argv.some((arg) => arg === "--help" || arg === "-h")) {
119
+ return { command: "help", flags: { subcommand: argv[0] } };
120
+ }
87
121
  const [command, ...rest] = argv;
88
122
  if (!command || command === "--help" || command === "-h") {
89
123
  return { command: "help", flags: {} };
@@ -158,6 +192,28 @@ function integerFlag(flags, name, defaultValue) {
158
192
  return value;
159
193
  }
160
194
 
195
+ function contextPctFlag(flags, name, defaultValue) {
196
+ const value = flags[name] === undefined ? defaultValue : Number(flags[name]);
197
+ if (!Number.isFinite(value) || value < 0 || value > 100)
198
+ throw new Error(`--${name} must be between 0 and 100`);
199
+ return value;
200
+ }
201
+
202
+ function contextOpts(flags, cli) {
203
+ return {
204
+ warnContextPct: contextPctFlag(
205
+ flags,
206
+ "warn-context-pct",
207
+ CONTEXT_THRESHOLDS[cli].warnContextPct,
208
+ ),
209
+ maxContextPct: contextPctFlag(
210
+ flags,
211
+ "max-context-pct",
212
+ CONTEXT_THRESHOLDS[cli].maxContextPct,
213
+ ),
214
+ };
215
+ }
216
+
161
217
  function selectAdapter(flags) {
162
218
  const cli = flags.cli ?? "codex";
163
219
  const adapter = ADAPTERS[cli];
@@ -233,7 +289,16 @@ function buildRemoteLiveArgv(verb, opts) {
233
289
  if (opts.busyTimeoutMs)
234
290
  args.push("--busy-timeout", String(opts.busyTimeoutMs / 1000));
235
291
  if (opts.maxTurnMs) args.push("--max-turn", String(opts.maxTurnMs / 1000));
236
- if (verb === "ask") args.push("--prompt", opts.prompt ?? "");
292
+ if (verb === "ask") {
293
+ args.push("--prompt", opts.prompt ?? "");
294
+ if (opts.noWait) args.push("--no-wait");
295
+ if (opts.noRelayTag) args.push("--no-relay-tag");
296
+ if (opts.requestId) args.push("--request-id", opts.requestId);
297
+ if (opts.maxContextPct !== undefined)
298
+ args.push("--max-context-pct", String(opts.maxContextPct));
299
+ if (opts.warnContextPct !== undefined)
300
+ args.push("--warn-context-pct", String(opts.warnContextPct));
301
+ }
237
302
  args.push("--timeout", timeoutSeconds(opts.timeoutMs));
238
303
  if (verb === "ask" && opts.settleMs) {
239
304
  args.push("--settle", String(opts.settleMs));
@@ -1665,10 +1730,10 @@ const ADAPTERS = {
1665
1730
  },
1666
1731
  };
1667
1732
 
1668
- function buildLaunchKeys(adapter, { model, effort } = {}) {
1733
+ function buildLaunchKeys(adapter, { model, effort, name } = {}) {
1669
1734
  const hasModel = model !== undefined;
1670
1735
  const hasEffort = effort !== undefined;
1671
- if (!hasModel && !hasEffort) {
1736
+ if (!hasModel && !hasEffort && !(adapter.cli === "claude" && name)) {
1672
1737
  return adapter.launchKeys;
1673
1738
  }
1674
1739
 
@@ -1684,6 +1749,7 @@ function buildLaunchKeys(adapter, { model, effort } = {}) {
1684
1749
  );
1685
1750
  }
1686
1751
  } else if (adapter.cli === "claude") {
1752
+ if (name) overrideArgs.push("-n", name);
1687
1753
  if (hasModel) {
1688
1754
  overrideArgs.push("--model", model);
1689
1755
  }
@@ -1971,12 +2037,23 @@ async function doStart(adapter, opts) {
1971
2037
  } = opts;
1972
2038
  if (session.includes(":"))
1973
2039
  throw new Error("start --session accepts only a session name without ':'");
2040
+ const now = new Date();
2041
+ const resumed = Boolean(resume || resumeLast);
2042
+ const name =
2043
+ opts.name ??
2044
+ (resumed ? null : `${now.getMonth() + 1}.${now.getDate()} ${session}`);
2045
+ if (
2046
+ name !== null &&
2047
+ (!/^\d{1,2}\.\d{1,2} \S/u.test(name) || /[\r\n]/u.test(name))
2048
+ )
2049
+ throw new Error("--name must use '<month>.<day> <topic>' on one line");
1974
2050
  const launchKeys = resume
1975
2051
  ? [adapter.resumeById(resume), "Enter"]
1976
2052
  : resumeLast
1977
2053
  ? [adapter.resumeLast(), "Enter"]
1978
- : buildLaunchKeys(adapter, { model, effort });
1979
- const resumed = Boolean(resume || resumeLast);
2054
+ : buildLaunchKeys(adapter, { model, effort, name });
2055
+ if (resumed && name && adapter.cli === "claude")
2056
+ launchKeys[0] += ` ${shellQuote(["-n", name])}`;
1980
2057
  const resumeTarget = resume ?? (resumeLast ? "last" : null);
1981
2058
 
1982
2059
  await runTmux(remote, [
@@ -2006,6 +2083,34 @@ async function doStart(adapter, opts) {
2006
2083
  readyTimeoutMs,
2007
2084
  pollIntervalMs,
2008
2085
  );
2086
+ let nameApplied = Boolean(name) && adapter.cli === "claude" && ready;
2087
+ if (name && adapter.cli === "codex" && ready) {
2088
+ try {
2089
+ await runTmux(remote, [
2090
+ "send-keys",
2091
+ "-t",
2092
+ session,
2093
+ "-l",
2094
+ "--",
2095
+ `/rename ${name}`,
2096
+ ]);
2097
+ await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
2098
+ const deadline = Date.now() + Math.min(readyTimeoutMs, 2000);
2099
+ do {
2100
+ const discovery = await discoverCodexTmuxSessions({ remote });
2101
+ nameApplied =
2102
+ discovery.sessions?.some(
2103
+ (entry) =>
2104
+ entry.session === session &&
2105
+ entry.panes.some((pane) => pane.name === name),
2106
+ ) === true;
2107
+ if (nameApplied || remote) break;
2108
+ await sleep(Math.min(pollIntervalMs, 200));
2109
+ } while (Date.now() < deadline);
2110
+ } catch {
2111
+ nameApplied = false;
2112
+ }
2113
+ }
2009
2114
  return addLoginGuard(adapter, raw, {
2010
2115
  cli: adapter.cli,
2011
2116
  session,
@@ -2013,6 +2118,15 @@ async function doStart(adapter, opts) {
2013
2118
  resumed,
2014
2119
  resumeTarget,
2015
2120
  ready,
2121
+ name,
2122
+ nameGenerated: !resumed && opts.name === undefined,
2123
+ nameApplied,
2124
+ ...(name && adapter.cli === "codex" && !nameApplied
2125
+ ? {
2126
+ nameWarning:
2127
+ "name update could not be confirmed from the Codex session registry",
2128
+ }
2129
+ : {}),
2016
2130
  raw,
2017
2131
  });
2018
2132
  }
@@ -2068,6 +2182,19 @@ async function doAskViaTmux(adapter, opts) {
2068
2182
  });
2069
2183
  const beforeRaw = await capturePane(remote, session);
2070
2184
  const contextPctBefore = adapter.contextPct(beforeRaw);
2185
+ const context = opts.skipContextGuard ? {} : await tmuxContext(adapter, opts);
2186
+ const guard = opts.skipContextGuard
2187
+ ? {}
2188
+ : contextGuard(adapter.cli, context, opts);
2189
+ if (guard.ok === false)
2190
+ return {
2191
+ ...guard,
2192
+ status: "failed",
2193
+ done: false,
2194
+ cli: adapter.cli,
2195
+ session,
2196
+ transport: "tmux",
2197
+ };
2071
2198
 
2072
2199
  const bufferName = tmuxBufferName(session);
2073
2200
  await runTmux(remote, ["set-buffer", "-b", bufferName, "--", prompt]);
@@ -2076,13 +2203,14 @@ async function doAskViaTmux(adapter, opts) {
2076
2203
  "-b",
2077
2204
  bufferName,
2078
2205
  "-d",
2206
+ "-p",
2079
2207
  "-t",
2080
2208
  session,
2081
2209
  ]);
2082
2210
  await sleep(settleMs);
2083
2211
 
2084
2212
  let promptSubmitted = !doneMarker;
2085
- if (doneMarker) {
2213
+ if (doneMarker || opts.noWait) {
2086
2214
  for (let attempt = 1; attempt <= PROMPT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
2087
2215
  await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
2088
2216
  await sleep(PROMPT_SUBMIT_RETRY_DELAY_MS);
@@ -2101,6 +2229,25 @@ async function doAskViaTmux(adapter, opts) {
2101
2229
  await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
2102
2230
  }
2103
2231
 
2232
+ if (opts.noWait) {
2233
+ return {
2234
+ ...context,
2235
+ ...guard,
2236
+ ok: true,
2237
+ cli: adapter.cli,
2238
+ transport: "tmux",
2239
+ session,
2240
+ remote: remote ?? null,
2241
+ status: "submitted",
2242
+ inputSent: true,
2243
+ done: false,
2244
+ submittedAt: new Date().toISOString(),
2245
+ target: session,
2246
+ ifBusy,
2247
+ busyWaitedMs,
2248
+ };
2249
+ }
2250
+
2104
2251
  const startedAt = Date.now();
2105
2252
  let raw = "";
2106
2253
  let visible = "";
@@ -2184,6 +2331,8 @@ async function doAskViaTmux(adapter, opts) {
2184
2331
  response,
2185
2332
  contextPctBefore,
2186
2333
  contextPctAfter,
2334
+ ...context,
2335
+ ...guard,
2187
2336
  matchedCompletion: done,
2188
2337
  done,
2189
2338
  raw,
@@ -2191,6 +2340,24 @@ async function doAskViaTmux(adapter, opts) {
2191
2340
  }
2192
2341
 
2193
2342
  async function doAsk(adapter, opts) {
2343
+ const requestId = opts.requestId ?? randomBytes(6).toString("hex");
2344
+ const tag = `[tfx-live req=${requestId}]`;
2345
+ const prompt =
2346
+ opts.noRelayTag || opts.prompt.startsWith(`${tag}\n`)
2347
+ ? opts.prompt
2348
+ : `${tag}\n${opts.prompt}`;
2349
+ try {
2350
+ return {
2351
+ ...(await dispatchAsk(adapter, { ...opts, prompt, requestId })),
2352
+ requestId,
2353
+ };
2354
+ } catch (error) {
2355
+ error.requestId = requestId;
2356
+ throw error;
2357
+ }
2358
+ }
2359
+
2360
+ async function dispatchAsk(adapter, opts) {
2194
2361
  const transport = opts.transport ?? "tmux";
2195
2362
  if (transport === "tmux") {
2196
2363
  return doAskViaTmux(adapter, opts);
@@ -2211,6 +2378,9 @@ async function doAsk(adapter, opts) {
2211
2378
  ifBusy: opts.ifBusy,
2212
2379
  busyTimeoutMs: opts.busyTimeoutMs,
2213
2380
  pollIntervalMs: opts.pollIntervalMs,
2381
+ noWait: opts.noWait,
2382
+ maxContextPct: opts.maxContextPct,
2383
+ warnContextPct: opts.warnContextPct,
2214
2384
  });
2215
2385
  }
2216
2386
 
@@ -2262,6 +2432,11 @@ async function doAskViaDaemon(opts, meta = {}) {
2262
2432
  if (short) payload.short = short;
2263
2433
  if (sessionId) payload.sessionId = sessionId;
2264
2434
  if (configDir) payload.configDir = configDir;
2435
+ if (opts.noWait) payload.noWait = true;
2436
+ if (opts.maxContextPct !== undefined)
2437
+ payload.maxContextPct = opts.maxContextPct;
2438
+ if (opts.warnContextPct !== undefined)
2439
+ payload.warnContextPct = opts.warnContextPct;
2265
2440
 
2266
2441
  const result = await callBridgeVerb(
2267
2442
  bridgePath,
@@ -2273,6 +2448,14 @@ async function doAskViaDaemon(opts, meta = {}) {
2273
2448
  // are surfaced but never counted as done.
2274
2449
  const matchedCompletion = result?.matchedCompletion === true;
2275
2450
  return {
2451
+ ...result,
2452
+ status:
2453
+ result?.status ??
2454
+ (matchedCompletion
2455
+ ? "completed"
2456
+ : result?.inputSent === false
2457
+ ? "failed"
2458
+ : "unknown"),
2276
2459
  cli: "claude",
2277
2460
  transport: "uds",
2278
2461
  short: short ?? null,
@@ -2280,9 +2463,11 @@ async function doAskViaDaemon(opts, meta = {}) {
2280
2463
  response: result?.text ?? "",
2281
2464
  raw: result?.raw ?? result?.responseRaw ?? "",
2282
2465
  matchedCompletion,
2283
- timedOut: result?.timedOut === true,
2466
+ ...(result?.status === "submitted"
2467
+ ? {}
2468
+ : { timedOut: result?.timedOut === true }),
2284
2469
  closed: result?.closed === true,
2285
- inputSent: result?.inputSent === true,
2470
+ inputSent: result?.inputSent ?? null,
2286
2471
  daemon: result?.daemon ?? null,
2287
2472
  daemons: result?.daemons ?? [],
2288
2473
  matches: result?.matches ?? [],
@@ -2431,11 +2616,21 @@ async function doAskAuto(adapter, opts) {
2431
2616
  {
2432
2617
  transportSelected: "uds",
2433
2618
  transportProbe: resolution.transportProbe,
2619
+ ...(probe?.raw?.recoveredFrom
2620
+ ? { recoveredFrom: probe.raw.recoveredFrom }
2621
+ : {}),
2434
2622
  },
2435
2623
  );
2436
- if (udsResult.matchedCompletion) {
2624
+ if (
2625
+ udsResult.matchedCompletion ||
2626
+ udsResult.status === "submitted" ||
2627
+ udsResult.errorCode === "context-limit"
2628
+ ) {
2437
2629
  return udsResult;
2438
2630
  }
2631
+ if (udsResult.inputSent !== false) {
2632
+ return { ...udsResult, status: "unknown", done: false };
2633
+ }
2439
2634
  // Daemon was reachable but the attach round did not complete (error,
2440
2635
  // timeout, or socket close). File a report async, then keep the user moving
2441
2636
  // on tmux instead of returning a dead uds result.
@@ -2497,6 +2692,34 @@ async function doAskAuto(adapter, opts) {
2497
2692
  }
2498
2693
 
2499
2694
  async function doStop(adapter, opts) {
2695
+ if (opts.short || opts.sessionId) {
2696
+ let sessionId = opts.sessionId;
2697
+ let configDir = opts.configDir;
2698
+ if (!sessionId) {
2699
+ const probe = await callBridgeVerb(
2700
+ opts.bridgePath,
2701
+ "daemon-probe",
2702
+ {
2703
+ short: opts.short,
2704
+ configDir: opts.configDir,
2705
+ },
2706
+ 10_000,
2707
+ );
2708
+ if (!probe.ok || !probe.target?.sessionId)
2709
+ throw new Error(
2710
+ probe.error || probe.reason || "Claude session not found",
2711
+ );
2712
+ sessionId = probe.target.sessionId;
2713
+ configDir = probe.daemon?.configDir ?? configDir;
2714
+ }
2715
+ await execFileAsync("claude", ["stop", sessionId], {
2716
+ timeout: 15_000,
2717
+ env: configDir
2718
+ ? { ...process.env, CLAUDE_CONFIG_DIR: configDir }
2719
+ : process.env,
2720
+ });
2721
+ return { cli: "claude", sessionId, stopped: true, conversationKept: true };
2722
+ }
2500
2723
  const { session, remote } = opts;
2501
2724
  if (/[:.]|^[%@]\d+$/.test(session))
2502
2725
  throw new Error(
@@ -2630,6 +2853,7 @@ function startOpts(flags) {
2630
2853
  resumeLast: Object.hasOwn(flags, "resume-last"),
2631
2854
  model: flags.model,
2632
2855
  effort: flags.effort,
2856
+ name: flags.name,
2633
2857
  readyTimeoutMs: secondsFlag(
2634
2858
  flags,
2635
2859
  "ready-timeout",
@@ -2701,6 +2925,10 @@ function askOpts(flags, adapter) {
2701
2925
  ),
2702
2926
  bridgePath: resolveBridgePath(flags),
2703
2927
  prompt: requireFlag(flags, "prompt"),
2928
+ requestId: flags["request-id"],
2929
+ noWait: Object.hasOwn(flags, "no-wait"),
2930
+ noRelayTag: Object.hasOwn(flags, "no-relay-tag"),
2931
+ ...contextOpts(flags, adapter.cli),
2704
2932
  remote: flags.remote,
2705
2933
  timeoutMs: secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
2706
2934
  maxTurnMs: codexUds
@@ -2716,8 +2944,22 @@ function askOpts(flags, adapter) {
2716
2944
  }
2717
2945
 
2718
2946
  function stopOpts(flags) {
2947
+ const targetCount = [flags.session, flags.short, flags["session-id"]].filter(
2948
+ Boolean,
2949
+ ).length;
2950
+ if (targetCount !== 1)
2951
+ throw new Error("stop requires one of --session, --short, or --session-id");
2952
+ if (
2953
+ (flags.short || flags["session-id"]) &&
2954
+ (flags.remote || (flags.cli && flags.cli !== "claude"))
2955
+ )
2956
+ throw new Error("stop --short/--session-id supports local Claude only");
2719
2957
  return {
2720
- session: requireFlag(flags, "session"),
2958
+ session: flags.session,
2959
+ short: flags.short,
2960
+ sessionId: flags["session-id"],
2961
+ configDir: flags["config-dir"],
2962
+ bridgePath: flags.short ? resolveBridgePath(flags) : undefined,
2721
2963
  remote: flags.remote,
2722
2964
  };
2723
2965
  }
@@ -2755,18 +2997,186 @@ async function ask(flags) {
2755
2997
  printJson(await doAsk(adapter, askOpts(flags, adapter)));
2756
2998
  }
2757
2999
 
3000
+ async function wait(flags) {
3001
+ if ((flags.cli ?? "claude") !== "claude")
3002
+ throw new Error(
3003
+ "wait supports Claude UDS only; Codex UDS wait is unavailable",
3004
+ );
3005
+ if (!flags.short && !flags["session-id"])
3006
+ throw new Error("wait requires --short or --session-id");
3007
+ if (flags.remote || (flags.transport && flags.transport !== "uds"))
3008
+ throw new Error("wait supports local Claude UDS only");
3009
+ const timeoutMs = secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS);
3010
+ printJson(
3011
+ await callBridgeVerb(
3012
+ resolveBridgePath(flags),
3013
+ "daemon-wait",
3014
+ {
3015
+ short: flags.short,
3016
+ sessionId: flags["session-id"],
3017
+ configDir: flags["config-dir"],
3018
+ requestId: flags["request-id"],
3019
+ timeoutMs,
3020
+ pollIntervalMs: msFlag(
3021
+ flags,
3022
+ "poll-interval",
3023
+ DEFAULT_POLL_INTERVAL_MS,
3024
+ ),
3025
+ },
3026
+ timeoutMs,
3027
+ ),
3028
+ );
3029
+ }
3030
+
3031
+ async function rename(flags) {
3032
+ if (flags.cli !== "codex" || flags.transport !== "uds" || flags.remote)
3033
+ throw new Error("rename requires local --cli codex --transport uds");
3034
+ const name = requireFlag(flags, "name").trim();
3035
+ if (!name) throw new Error("--name must not be blank");
3036
+ const threadId = requireFlag(flags, "thread");
3037
+ if (threadId === "auto")
3038
+ throw new Error("rename requires an explicit --thread ID");
3039
+ const { renameCodexAppServerThread } = await import(
3040
+ "../hub/team/uds-orchestrator.mjs"
3041
+ );
3042
+ printJson(
3043
+ await renameCodexAppServerThread({
3044
+ socketPath: resolveCodexDaemonSocket(flags["codex-socket"] ?? "default"),
3045
+ threadId,
3046
+ name,
3047
+ }),
3048
+ );
3049
+ }
3050
+
2758
3051
  async function stop(flags) {
2759
- const adapter = selectAdapter(flags);
3052
+ const adapter = selectAdapter({
3053
+ ...flags,
3054
+ cli: flags.cli ?? (flags.short || flags["session-id"] ? "claude" : "codex"),
3055
+ });
2760
3056
  printJson(await doStop(adapter, stopOpts(flags)));
2761
3057
  }
2762
3058
 
3059
+ async function claudeTmuxTranscript(session, configDir) {
3060
+ const { stdout: paneId } = await runTmux(null, [
3061
+ "display-message",
3062
+ "-p",
3063
+ "-t",
3064
+ session,
3065
+ "#{pane_id}",
3066
+ ]);
3067
+ const discovery = await discoverClaudeTmuxSessions();
3068
+ const target = discovery.sessions.find(
3069
+ (entry) => entry.paneId === paneId.trim(),
3070
+ );
3071
+ return findClaudeTranscript({
3072
+ configDir:
3073
+ configDir ||
3074
+ process.env.CLAUDE_CONFIG_DIR ||
3075
+ pathJoin(homedir(), ".claude"),
3076
+ sessionId: target?.sessionId,
3077
+ cwd: target?.cwd,
3078
+ });
3079
+ }
3080
+
3081
+ async function tmuxContext(adapter, opts) {
3082
+ const unknown = modelContext(adapter.cli, null);
3083
+ if (opts.remote) return unknown;
3084
+ try {
3085
+ if (adapter.cli === "claude") {
3086
+ const transcript = await claudeTmuxTranscript(
3087
+ opts.session,
3088
+ opts.configDir,
3089
+ );
3090
+ return (await readClaudeTranscript(transcript))?.context ?? unknown;
3091
+ }
3092
+ const { stdout: target } = await runTmux(null, [
3093
+ "display-message",
3094
+ "-p",
3095
+ "-t",
3096
+ opts.session,
3097
+ "#{session_name}:#{window_index}.#{pane_index}",
3098
+ ]);
3099
+ const discovery = await discoverCodexTmuxSessions();
3100
+ const pane = discovery.sessions
3101
+ .flatMap((entry) => entry.panes)
3102
+ .find((entry) => entry.target === target.trim());
3103
+ return await readCodexContext(null, pane?.threadId);
3104
+ } catch {
3105
+ return unknown;
3106
+ }
3107
+ }
3108
+
3109
+ async function compact(flags) {
3110
+ if (
3111
+ (flags.cli ?? "claude") !== "claude" ||
3112
+ flags.remote ||
3113
+ flags.short ||
3114
+ flags["session-id"] ||
3115
+ (flags.transport && flags.transport !== "tmux")
3116
+ )
3117
+ throw new Error(
3118
+ "compact supports local --cli claude --session NAME only; UDS slash execution is unverified",
3119
+ );
3120
+ const ifBusy = flags["if-busy"] ?? "fail";
3121
+ if (!["fail", "wait"].includes(ifBusy))
3122
+ throw new Error("compact --if-busy must be fail or wait");
3123
+ const opts = askOpts(
3124
+ {
3125
+ ...flags,
3126
+ cli: "claude",
3127
+ transport: "tmux",
3128
+ "if-busy": ifBusy,
3129
+ "no-wait": true,
3130
+ prompt: `/compact${flags.instructions ? ` ${flags.instructions}` : ""}`,
3131
+ },
3132
+ ADAPTERS.claude,
3133
+ );
3134
+ if (ifBusy === "wait") await waitForTmuxIdle(ADAPTERS.claude, opts);
3135
+ const transcriptPath = await claudeTmuxTranscript(
3136
+ opts.session,
3137
+ opts.configDir,
3138
+ );
3139
+ const before = await readClaudeTranscript(transcriptPath);
3140
+ if (!before)
3141
+ throw new Error(
3142
+ "compact requires a discoverable Claude transcript for this tmux pane",
3143
+ );
3144
+ await doAskViaTmux(ADAPTERS.claude, {
3145
+ ...opts,
3146
+ ifBusy: "fail",
3147
+ skipContextGuard: true,
3148
+ });
3149
+ const deadline = Date.now() + opts.timeoutMs;
3150
+ do {
3151
+ const snapshot = await readClaudeTranscript(transcriptPath);
3152
+ if (snapshot?.compactCount > before.compactCount) {
3153
+ printJson({
3154
+ cli: "claude",
3155
+ session: opts.session,
3156
+ compacted: true,
3157
+ ...snapshot.compact,
3158
+ });
3159
+ return;
3160
+ }
3161
+ await sleep(
3162
+ Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now())),
3163
+ );
3164
+ } while (Date.now() < deadline);
3165
+ printJson({
3166
+ cli: "claude",
3167
+ session: opts.session,
3168
+ compacted: false,
3169
+ timedOut: true,
3170
+ });
3171
+ }
3172
+
2763
3173
  async function interrupt(flags) {
2764
3174
  const adapter = selectAdapter(flags);
2765
3175
  printJson(await doInterrupt(adapter, interruptOpts(flags, adapter)));
2766
3176
  }
2767
3177
 
2768
3178
  async function probe(flags) {
2769
- const payload = {};
3179
+ const payload = { includeContext: true };
2770
3180
  if (flags.short) payload.short = flags.short;
2771
3181
  if (flags["session-id"]) payload.sessionId = flags["session-id"];
2772
3182
  if (flags["config-dir"]) payload.configDir = flags["config-dir"];
@@ -2822,6 +3232,7 @@ async function listSessions(flags) {
2822
3232
  function startOptsForSession(flags, session, side) {
2823
3233
  return {
2824
3234
  session,
3235
+ ...(!side ? { name: flags.name } : {}),
2825
3236
  remote: flags.remote,
2826
3237
  cwd: flags.cwd,
2827
3238
  ...(side
@@ -2839,10 +3250,12 @@ function startOptsForSession(flags, session, side) {
2839
3250
  };
2840
3251
  }
2841
3252
 
2842
- function askOptsForSession(flags, session, prompt) {
3253
+ function askOptsForSession(flags, session, prompt, cli) {
2843
3254
  return {
2844
3255
  session,
2845
3256
  prompt,
3257
+ ...contextOpts(flags, cli),
3258
+ noRelayTag: Object.hasOwn(flags, "no-relay-tag"),
2846
3259
  remote: flags.remote,
2847
3260
  timeoutMs: secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
2848
3261
  settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
@@ -2894,6 +3307,7 @@ async function stopAfterLifecycle(stopSpecs, primaryError = null) {
2894
3307
 
2895
3308
  function turnFromAsk(prompt, result) {
2896
3309
  return {
3310
+ requestId: result.requestId,
2897
3311
  prompt,
2898
3312
  response: result.response,
2899
3313
  done: result.done,
@@ -2924,7 +3338,7 @@ async function converse(flags) {
2924
3338
  for (const prompt of prompts) {
2925
3339
  const result = await doAsk(
2926
3340
  adapter,
2927
- askOptsForSession(flags, session, prompt),
3341
+ askOptsForSession(flags, session, prompt, adapter.cli),
2928
3342
  );
2929
3343
  turns.push(turnFromAsk(prompt, result));
2930
3344
  }
@@ -2965,7 +3379,7 @@ async function goalDriven(flags) {
2965
3379
  : `Continue. Reply with only ${doneToken} on its own line when fully complete.`;
2966
3380
  const result = await doAsk(
2967
3381
  adapter,
2968
- askOptsForSession(flags, session, prompt),
3382
+ askOptsForSession(flags, session, prompt, adapter.cli),
2969
3383
  );
2970
3384
  turns.push(turnFromAsk(prompt, result));
2971
3385
  if (hasDoneToken(result.response, doneToken)) {
@@ -3344,6 +3758,8 @@ function peerSideBaseOpts(flags, side, adapter, session) {
3344
3758
  }
3345
3759
  return {
3346
3760
  session,
3761
+ noRelayTag: Object.hasOwn(flags, "no-relay-tag"),
3762
+ ...contextOpts(flags, adapter.cli),
3347
3763
  short,
3348
3764
  sessionId,
3349
3765
  threadId,
@@ -3592,6 +4008,7 @@ async function peer(flags) {
3592
4008
 
3593
4009
  hops.push({
3594
4010
  hop: hopIndex + 1,
4011
+ requestId: result.requestId,
3595
4012
  from: isA ? "a" : "b",
3596
4013
  cli: adapter.cli,
3597
4014
  transport: result.transport ?? base.transport,
@@ -3767,7 +4184,7 @@ async function main() {
3767
4184
  const { command, flags } = parseCli(process.argv.slice(2));
3768
4185
 
3769
4186
  if (command === "help") {
3770
- process.stdout.write(`${usage()}\n`);
4187
+ process.stdout.write(`${usage(flags.subcommand)}\n`);
3771
4188
  return;
3772
4189
  }
3773
4190
 
@@ -3775,6 +4192,12 @@ async function main() {
3775
4192
  await start(flags);
3776
4193
  } else if (command === "ask") {
3777
4194
  await ask(flags);
4195
+ } else if (command === "wait") {
4196
+ await wait(flags);
4197
+ } else if (command === "compact") {
4198
+ await compact(flags);
4199
+ } else if (command === "rename") {
4200
+ await rename(flags);
3778
4201
  } else if (command === "stop") {
3779
4202
  await stop(flags);
3780
4203
  } else if (command === "interrupt") {
@@ -3825,6 +4248,7 @@ export {
3825
4248
  resolveAskTransport,
3826
4249
  resolveCodexDaemonSocket,
3827
4250
  splitTmuxTarget,
4251
+ stopOpts,
3828
4252
  tmuxBufferName,
3829
4253
  verifyAttachedPeerSide,
3830
4254
  };
@@ -3846,6 +4270,7 @@ if (isMainModule()) {
3846
4270
  printJson({
3847
4271
  ok: false,
3848
4272
  error: error.message,
4273
+ ...(error.requestId ? { requestId: error.requestId } : {}),
3849
4274
  });
3850
4275
  process.exitCode = 1;
3851
4276
  });