triflux 10.3.4 → 10.7.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 (329) hide show
  1. package/LICENSE +21 -21
  2. package/bin/tfx-doctor-tui.mjs +1 -1
  3. package/bin/tfx-doctor.mjs +6 -1
  4. package/bin/tfx-profile.mjs +1 -1
  5. package/bin/tfx-setup-tui.mjs +1 -1
  6. package/bin/tfx-setup.mjs +6 -1
  7. package/bin/triflux.mjs +2396 -1140
  8. package/hooks/agent-route-guard.mjs +12 -8
  9. package/hooks/cross-review-tracker.mjs +21 -8
  10. package/hooks/error-context.mjs +19 -7
  11. package/hooks/hook-adaptive-collector.mjs +18 -16
  12. package/hooks/hook-manager.mjs +93 -32
  13. package/hooks/hook-orchestrator.mjs +108 -24
  14. package/hooks/hook-registry.json +11 -0
  15. package/hooks/keyword-rules.json +6 -10
  16. package/hooks/lib/resolve-root.mjs +1 -1
  17. package/hooks/mcp-config-watcher.mjs +6 -2
  18. package/hooks/pipeline-stop.mjs +3 -6
  19. package/hooks/safety-guard.mjs +99 -28
  20. package/hooks/session-start-fast.mjs +143 -0
  21. package/hooks/subagent-verifier.mjs +5 -4
  22. package/hub/account-broker.mjs +256 -60
  23. package/hub/adaptive-diagnostic.mjs +75 -48
  24. package/hub/adaptive-inject.mjs +95 -57
  25. package/hub/adaptive-memory.mjs +156 -42
  26. package/hub/adaptive.mjs +60 -31
  27. package/hub/assign-callbacks.mjs +67 -30
  28. package/hub/bridge.mjs +0 -1
  29. package/hub/cli-adapter-base.mjs +200 -48
  30. package/hub/codex-adapter.mjs +76 -96
  31. package/hub/codex-compat.mjs +3 -3
  32. package/hub/codex-preflight.mjs +63 -37
  33. package/hub/delegator/contracts.mjs +19 -23
  34. package/hub/delegator/index.mjs +3 -3
  35. package/hub/delegator/service.mjs +88 -64
  36. package/hub/delegator/tool-definitions.mjs +5 -5
  37. package/hub/fullcycle.mjs +33 -17
  38. package/hub/gemini-adapter.mjs +69 -94
  39. package/hub/hitl.mjs +89 -30
  40. package/hub/intent.mjs +161 -38
  41. package/hub/lib/cache-guard.mjs +43 -17
  42. package/hub/lib/mcp-response-cache.mjs +66 -32
  43. package/hub/lib/memory-store.mjs +285 -111
  44. package/hub/lib/path-utils.mjs +35 -37
  45. package/hub/lib/process-utils.mjs +106 -37
  46. package/hub/lib/spawn-trace.mjs +527 -0
  47. package/hub/lib/ssh-command.mjs +34 -4
  48. package/hub/lib/ssh-retry.mjs +5 -1
  49. package/hub/lib/uuidv7.mjs +4 -3
  50. package/hub/memory-doctor.mjs +266 -106
  51. package/hub/middleware/request-logger.mjs +61 -34
  52. package/hub/paths.mjs +9 -9
  53. package/hub/pipeline/gates/confidence.mjs +34 -15
  54. package/hub/pipeline/gates/consensus.mjs +27 -15
  55. package/hub/pipeline/gates/index.mjs +7 -3
  56. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  57. package/hub/pipeline/index.mjs +77 -42
  58. package/hub/pipeline/state.mjs +10 -10
  59. package/hub/pipeline/transitions.mjs +40 -23
  60. package/hub/platform.mjs +57 -48
  61. package/hub/promote-penalties.mjs +25 -7
  62. package/hub/quality/deslop.mjs +70 -49
  63. package/hub/research.mjs +32 -25
  64. package/hub/router.mjs +240 -107
  65. package/hub/routing/complexity.mjs +132 -29
  66. package/hub/routing/index.mjs +17 -12
  67. package/hub/routing/q-learning.mjs +76 -28
  68. package/hub/server.mjs +4 -4
  69. package/hub/session-fingerprint.mjs +126 -60
  70. package/hub/state.mjs +84 -43
  71. package/hub/store-adapter.mjs +59 -26
  72. package/hub/store.mjs +356 -153
  73. package/hub/team/agent-map.json +22 -7
  74. package/hub/team/ansi.mjs +186 -122
  75. package/hub/team/backend.mjs +28 -10
  76. package/hub/team/cli/commands/attach.mjs +29 -9
  77. package/hub/team/cli/commands/control.mjs +29 -8
  78. package/hub/team/cli/commands/debug.mjs +32 -11
  79. package/hub/team/cli/commands/focus.mjs +38 -11
  80. package/hub/team/cli/commands/interrupt.mjs +18 -6
  81. package/hub/team/cli/commands/kill.mjs +16 -5
  82. package/hub/team/cli/commands/list.mjs +11 -4
  83. package/hub/team/cli/commands/send.mjs +19 -6
  84. package/hub/team/cli/commands/start/index.mjs +154 -31
  85. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  86. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  87. package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
  88. package/hub/team/cli/commands/start/start-mux.mjs +70 -21
  89. package/hub/team/cli/commands/start/start-wt.mjs +29 -12
  90. package/hub/team/cli/commands/status.mjs +43 -14
  91. package/hub/team/cli/commands/stop.mjs +11 -4
  92. package/hub/team/cli/commands/task.mjs +8 -3
  93. package/hub/team/cli/commands/tasks.mjs +1 -1
  94. package/hub/team/cli/index.mjs +2 -2
  95. package/hub/team/cli/manifest.mjs +38 -8
  96. package/hub/team/cli/render.mjs +30 -8
  97. package/hub/team/cli/services/attach-fallback.mjs +31 -11
  98. package/hub/team/cli/services/hub-client.mjs +42 -14
  99. package/hub/team/cli/services/member-selector.mjs +11 -4
  100. package/hub/team/cli/services/native-control.mjs +48 -21
  101. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  102. package/hub/team/cli/services/state-store.mjs +25 -8
  103. package/hub/team/cli/services/task-model.mjs +16 -6
  104. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  105. package/hub/team/conductor.mjs +8 -4
  106. package/hub/team/dashboard-anchor.mjs +4 -5
  107. package/hub/team/dashboard-layout.mjs +3 -1
  108. package/hub/team/dashboard-open.mjs +41 -21
  109. package/hub/team/dashboard.mjs +76 -28
  110. package/hub/team/event-log.mjs +18 -10
  111. package/hub/team/handoff.mjs +31 -15
  112. package/hub/team/headless.mjs +2 -1
  113. package/hub/team/health-probe.mjs +69 -54
  114. package/hub/team/launcher-template.mjs +16 -13
  115. package/hub/team/native-supervisor.mjs +65 -21
  116. package/hub/team/native.mjs +74 -35
  117. package/hub/team/nativeProxy.mjs +184 -113
  118. package/hub/team/notify.mjs +119 -76
  119. package/hub/team/orchestrator.mjs +9 -4
  120. package/hub/team/pane.mjs +12 -7
  121. package/hub/team/process-cleanup.mjs +25 -16
  122. package/hub/team/psmux.mjs +491 -201
  123. package/hub/team/remote-probe.mjs +68 -52
  124. package/hub/team/remote-session.mjs +117 -59
  125. package/hub/team/remote-watcher.mjs +61 -33
  126. package/hub/team/routing.mjs +51 -25
  127. package/hub/team/runtime-strategy.mjs +3 -1
  128. package/hub/team/session.mjs +98 -34
  129. package/hub/team/staleState.mjs +72 -30
  130. package/hub/team/swarm-locks.mjs +15 -13
  131. package/hub/team/swarm-planner.mjs +32 -21
  132. package/hub/team/swarm-reconciler.mjs +48 -23
  133. package/hub/team/tui-lite.mjs +266 -68
  134. package/hub/team/tui-remote-adapter.mjs +14 -10
  135. package/hub/team/tui-viewer.mjs +99 -43
  136. package/hub/team/tui.mjs +708 -271
  137. package/hub/team/worktree-lifecycle.mjs +152 -58
  138. package/hub/team/wt-manager.mjs +24 -14
  139. package/hub/token-mode.mjs +71 -71
  140. package/hub/tray.mjs +66 -23
  141. package/hub/workers/claude-worker.mjs +162 -118
  142. package/hub/workers/codex-mcp.mjs +192 -141
  143. package/hub/workers/delegator-mcp.mjs +507 -333
  144. package/hub/workers/factory.mjs +8 -8
  145. package/hub/workers/gemini-worker.mjs +115 -84
  146. package/hub/workers/interface.mjs +6 -1
  147. package/hub/workers/worker-utils.mjs +21 -14
  148. package/hud/colors.mjs +27 -9
  149. package/hud/constants.mjs +162 -26
  150. package/hud/context-monitor.mjs +82 -41
  151. package/hud/hud-qos-status.mjs +129 -49
  152. package/hud/mission-board.mjs +6 -3
  153. package/hud/providers/claude.mjs +226 -115
  154. package/hud/providers/codex.mjs +62 -22
  155. package/hud/providers/gemini.mjs +168 -56
  156. package/hud/renderers.mjs +384 -119
  157. package/hud/terminal.mjs +101 -31
  158. package/hud/utils.mjs +78 -38
  159. package/mesh/index.mjs +11 -5
  160. package/mesh/mesh-budget.mjs +18 -9
  161. package/mesh/mesh-heartbeat.mjs +1 -1
  162. package/mesh/mesh-queue.mjs +3 -5
  163. package/mesh/mesh-router.mjs +5 -4
  164. package/package.json +2 -1
  165. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  166. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  167. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  168. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  169. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  170. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  171. package/scripts/__tests__/skill-template.test.mjs +98 -50
  172. package/scripts/__tests__/smoke.test.mjs +1 -1
  173. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  174. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  175. package/scripts/cache-doctor.mjs +11 -4
  176. package/scripts/cache-warmup.mjs +96 -37
  177. package/scripts/claudemd-sync.mjs +27 -17
  178. package/scripts/codex-gateway-preflight.mjs +52 -37
  179. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  180. package/scripts/completions/tfx.bash +47 -47
  181. package/scripts/completions/tfx.fish +44 -44
  182. package/scripts/completions/tfx.zsh +83 -83
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +45 -26
  193. package/scripts/keyword-detector.mjs +41 -20
  194. package/scripts/keyword-rules-expander.mjs +47 -30
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +300 -63
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +199 -84
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
  229. package/scripts/tfx-batch-stats.mjs +32 -11
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  237. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  238. package/skills/.omc/state/last-tool-error.json +7 -0
  239. package/skills/.omc/state/subagent-tracking.json +7 -0
  240. package/skills/_templates/base.md +1 -6
  241. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  242. package/skills/shared/telemetry-segment.md +6 -0
  243. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  244. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  245. package/skills/tfx-analysis/skill.json +1 -6
  246. package/skills/tfx-auto/SKILL.md +1 -0
  247. package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
  248. package/skills/tfx-auto-codex/skill.json +1 -3
  249. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  250. package/skills/tfx-autopilot/skill.json +1 -5
  251. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  252. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  253. package/skills/tfx-autoroute/skill.json +1 -7
  254. package/skills/tfx-codex/SKILL.md +1 -0
  255. package/skills/tfx-codex/skill.json +1 -3
  256. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  257. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  258. package/skills/tfx-codex-swarm/skill.json +1 -4
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  265. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  266. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  267. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  268. package/skills/tfx-debate/skill.json +1 -7
  269. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  270. package/skills/tfx-deep-analysis/skill.json +1 -5
  271. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  272. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  273. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  274. package/skills/tfx-deep-qa/skill.json +1 -6
  275. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  276. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  277. package/skills/tfx-doctor/SKILL.md +21 -0
  278. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  279. package/skills/tfx-doctor/skill.json +1 -3
  280. package/skills/tfx-find/SKILL.md +1 -0
  281. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  282. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  283. package/skills/tfx-fullcycle/skill.json +1 -6
  284. package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
  285. package/skills/tfx-gemini/skill.json +1 -3
  286. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  287. package/skills/tfx-hooks/skill.json +1 -3
  288. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  289. package/skills/tfx-hub/skill.json +1 -3
  290. package/skills/tfx-index/SKILL.md +1 -0
  291. package/skills/tfx-index/skill.json +1 -6
  292. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  293. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  294. package/skills/tfx-multi/skill.json +1 -3
  295. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  296. package/skills/tfx-panel/skill.json +1 -7
  297. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  298. package/skills/tfx-persist/skill.json +1 -7
  299. package/skills/tfx-plan/SKILL.md +1 -0
  300. package/skills/tfx-plan/skill.json +1 -6
  301. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  302. package/skills/tfx-profile/skill.json +1 -3
  303. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  304. package/skills/tfx-prune/skill.json +1 -7
  305. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  306. package/skills/tfx-psmux-rules/skill.json +1 -4
  307. package/skills/tfx-qa/SKILL.md +1 -0
  308. package/skills/tfx-qa/skill.json +1 -6
  309. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  310. package/skills/tfx-ralph/skill.json +1 -4
  311. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  312. package/skills/tfx-remote-setup/skill.json +1 -3
  313. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  314. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
  315. package/skills/tfx-remote-spawn/skill.json +1 -4
  316. package/skills/tfx-research/SKILL.md +1 -0
  317. package/skills/tfx-review/SKILL.md +1 -0
  318. package/skills/tfx-review/skill.json +1 -6
  319. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  320. package/skills/tfx-setup/skill.json +1 -3
  321. package/skills/tfx-swarm/SKILL.md +22 -0
  322. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  323. package/tui/codex-profile.mjs +88 -33
  324. package/tui/core.mjs +45 -15
  325. package/tui/doctor.mjs +75 -28
  326. package/tui/gemini-profile.mjs +74 -29
  327. package/tui/monitor-data.mjs +8 -4
  328. package/tui/monitor.mjs +71 -27
  329. package/tui/setup.mjs +133 -42
package/scripts/run.cjs CHANGED
@@ -7,9 +7,11 @@ const { dirname, isAbsolute, join, resolve } = require("path");
7
7
  const { homedir } = require("os");
8
8
 
9
9
  function isValidPluginRoot(candidate) {
10
- return typeof candidate === "string"
11
- && candidate.trim().length > 0
12
- && existsSync(join(candidate.trim(), "hooks", "hook-orchestrator.mjs"));
10
+ return (
11
+ typeof candidate === "string" &&
12
+ candidate.trim().length > 0 &&
13
+ existsSync(join(candidate.trim(), "hooks", "hook-orchestrator.mjs"))
14
+ );
13
15
  }
14
16
 
15
17
  function resolvePluginRoot() {
@@ -23,7 +25,8 @@ function resolvePluginRoot() {
23
25
  }
24
26
  }
25
27
 
26
- if (isValidPluginRoot(process.env.CLAUDE_PLUGIN_ROOT)) return process.env.CLAUDE_PLUGIN_ROOT;
28
+ if (isValidPluginRoot(process.env.CLAUDE_PLUGIN_ROOT))
29
+ return process.env.CLAUDE_PLUGIN_ROOT;
27
30
  return dirname(__dirname);
28
31
  }
29
32
 
@@ -68,7 +71,7 @@ try {
68
71
  env: process.env,
69
72
  stdio: ["pipe", "inherit", "inherit"],
70
73
  input: stdinBuffer,
71
- windowsHide: true
74
+ windowsHide: true,
72
75
  });
73
76
  process.exit(0);
74
77
  } catch (error) {
@@ -1,14 +1,37 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, execFileSync } from "node:child_process";
2
+ import { execFileSync } from "node:child_process";
3
+ import { spawn } from "../hub/lib/spawn-trace.mjs";
3
4
 
4
5
  const SESSION_PREFIX = "tfx-isolated";
5
6
  const DEFAULT_ATTACH_PROFILE = "triflux";
6
7
  const SESSION_EXPIRE_MS = 30 * 60 * 1000;
7
8
 
8
9
  const STOP_WORDS = new Set([
9
- "a", "an", "and", "as", "at", "be", "by", "for", "from", "in",
10
- "is", "it", "of", "on", "or", "that", "the", "to", "with",
11
- "작업", "요청", "합니다", "그리고", "에서", "으로",
10
+ "a",
11
+ "an",
12
+ "and",
13
+ "as",
14
+ "at",
15
+ "be",
16
+ "by",
17
+ "for",
18
+ "from",
19
+ "in",
20
+ "is",
21
+ "it",
22
+ "of",
23
+ "on",
24
+ "or",
25
+ "that",
26
+ "the",
27
+ "to",
28
+ "with",
29
+ "작업",
30
+ "요청",
31
+ "합니다",
32
+ "그리고",
33
+ "에서",
34
+ "으로",
12
35
  ]);
13
36
 
14
37
  // ── psmux helpers ──
@@ -17,7 +40,9 @@ function hasPsmux() {
17
40
  try {
18
41
  execFileSync("psmux", ["-V"], { timeout: 2000, stdio: "ignore" });
19
42
  return true;
20
- } catch { return false; }
43
+ } catch {
44
+ return false;
45
+ }
21
46
  }
22
47
 
23
48
  function psmux(...args) {
@@ -27,16 +52,24 @@ function psmux(...args) {
27
52
  function psmuxCapture(sessionName) {
28
53
  try {
29
54
  return execFileSync("psmux", ["capture-pane", "-t", sessionName, "-p"], {
30
- timeout: 5000, encoding: "utf8",
55
+ timeout: 5000,
56
+ encoding: "utf8",
31
57
  }).trim();
32
- } catch { return ""; }
58
+ } catch {
59
+ return "";
60
+ }
33
61
  }
34
62
 
35
63
  function psmuxHasSession(sessionName) {
36
64
  try {
37
- execFileSync("psmux", ["has-session", "-t", sessionName], { timeout: 2000, stdio: "ignore" });
65
+ execFileSync("psmux", ["has-session", "-t", sessionName], {
66
+ timeout: 2000,
67
+ stdio: "ignore",
68
+ });
38
69
  return true;
39
- } catch { return false; }
70
+ } catch {
71
+ return false;
72
+ }
40
73
  }
41
74
 
42
75
  // ── core functions ──
@@ -58,7 +91,13 @@ export function createIsolatedSession(options = {}) {
58
91
  // send prompt as claude command
59
92
  if (options.prompt) {
60
93
  const safePrompt = options.prompt.replace(/'/g, "'\\''");
61
- psmux("send-keys", "-t", sessionName, `claude --prompt '${safePrompt}'`, "Enter");
94
+ psmux(
95
+ "send-keys",
96
+ "-t",
97
+ sessionName,
98
+ `claude --prompt '${safePrompt}'`,
99
+ "Enter",
100
+ );
62
101
  }
63
102
 
64
103
  return { sessionName };
@@ -70,8 +109,23 @@ export function attachWithWindowsTerminal(sessionName, options = {}) {
70
109
  const spawnFn = options.spawnFn || spawn;
71
110
 
72
111
  // sp (split-pane), not new-tab
73
- const wtArgs = ["sp", "-p", profile, "--title", title, "--", "psmux", "attach-session", "-t", sessionName];
74
- const child = spawnFn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false });
112
+ const wtArgs = [
113
+ "sp",
114
+ "-p",
115
+ profile,
116
+ "--title",
117
+ title,
118
+ "--",
119
+ "psmux",
120
+ "attach-session",
121
+ "-t",
122
+ sessionName,
123
+ ];
124
+ const child = spawnFn("wt.exe", wtArgs, {
125
+ detached: true,
126
+ stdio: "ignore",
127
+ windowsHide: false,
128
+ });
75
129
  child.unref();
76
130
  return wtArgs;
77
131
  }
@@ -86,7 +140,9 @@ export function waitForCompletion(sessionName, opts = {}) {
86
140
  if (!psmuxHasSession(sessionName) || Date.now() - start > maxMs) {
87
141
  const output = psmuxCapture(sessionName);
88
142
  // cleanup expired session
89
- try { psmux("kill-session", "-t", sessionName); } catch {}
143
+ try {
144
+ psmux("kill-session", "-t", sessionName);
145
+ } catch {}
90
146
  res({ sessionName, output, expired: Date.now() - start > maxMs });
91
147
  return;
92
148
  }
@@ -99,34 +155,60 @@ export function waitForCompletion(sessionName, opts = {}) {
99
155
  // ── context drift (kept from codex) ──
100
156
 
101
157
  function tokenize(text) {
102
- return String(text || "").toLowerCase()
158
+ return String(text || "")
159
+ .toLowerCase()
103
160
  .split(/[^\p{L}\p{N}_-]+/u)
104
161
  .filter((t) => t.length >= 2 && !STOP_WORDS.has(t));
105
162
  }
106
163
 
107
164
  export function evaluateContextDrift(input = {}) {
108
165
  const taskTokens = Array.from(new Set(tokenize(input.taskPrompt)));
109
- if (!taskTokens.length) return { drift: false, overlapRatio: 1, reason: "task-token-empty" };
166
+ if (!taskTokens.length)
167
+ return { drift: false, overlapRatio: 1, reason: "task-token-empty" };
110
168
 
111
169
  const outputTokens = new Set(tokenize(input.latestOutput));
112
170
  const matched = taskTokens.filter((t) => outputTokens.has(t));
113
171
  const ratio = matched.length / taskTokens.length;
114
172
  const threshold = input.minOverlapRatio ?? 0.2;
115
173
 
116
- return { drift: ratio < threshold, overlapRatio: ratio, reason: ratio < threshold ? "token-overlap-low" : "token-overlap-ok" };
174
+ return {
175
+ drift: ratio < threshold,
176
+ overlapRatio: ratio,
177
+ reason: ratio < threshold ? "token-overlap-low" : "token-overlap-ok",
178
+ };
117
179
  }
118
180
 
119
181
  // ── CLI ──
120
182
 
121
183
  function parseArgs(argv) {
122
- const a = { spawn: false, prompt: "", attach: false, background: false, name: "" };
184
+ const a = {
185
+ spawn: false,
186
+ prompt: "",
187
+ attach: false,
188
+ background: false,
189
+ name: "",
190
+ };
123
191
  for (let i = 2; i < argv.length; i++) {
124
192
  const arg = argv[i];
125
- if (arg === "--spawn") { a.spawn = true; continue; }
126
- if (arg === "--attach") { a.attach = true; continue; }
127
- if (arg === "--background") { a.background = true; continue; }
128
- if ((arg === "--prompt" || arg === "-p") && argv[i + 1]) { a.prompt = argv[++i]; continue; }
129
- if ((arg === "--name" || arg === "-n") && argv[i + 1]) { a.name = argv[++i]; }
193
+ if (arg === "--spawn") {
194
+ a.spawn = true;
195
+ continue;
196
+ }
197
+ if (arg === "--attach") {
198
+ a.attach = true;
199
+ continue;
200
+ }
201
+ if (arg === "--background") {
202
+ a.background = true;
203
+ continue;
204
+ }
205
+ if ((arg === "--prompt" || arg === "-p") && argv[i + 1]) {
206
+ a.prompt = argv[++i];
207
+ continue;
208
+ }
209
+ if ((arg === "--name" || arg === "-n") && argv[i + 1]) {
210
+ a.name = argv[++i];
211
+ }
130
212
  }
131
213
  return a;
132
214
  }
@@ -135,25 +217,29 @@ async function main() {
135
217
  const args = parseArgs(process.argv);
136
218
 
137
219
  if (!args.spawn) {
138
- process.stdout.write([
139
- "session-spawn-helper: psmux 격리 세션 생성 도구",
140
- "",
141
- "사용법:",
142
- " node scripts/session-spawn-helper.mjs --spawn --prompt '작업 내용' [--attach] [--background] [--name 세션명]",
143
- "",
144
- "옵션:",
145
- " --spawn 세션 생성 (필수)",
146
- " --prompt TEXT Claude에 전달할 프롬프트",
147
- " --attach WT split-pane으로 attach",
148
- " --background attach 없이 실행, 완료 시 결과 출력",
149
- " --name NAME 세션 이름 (기본: tfx-isolated-{ts})",
150
- "",
151
- ].join("\n"));
220
+ process.stdout.write(
221
+ [
222
+ "session-spawn-helper: psmux 격리 세션 생성 도구",
223
+ "",
224
+ "사용법:",
225
+ " node scripts/session-spawn-helper.mjs --spawn --prompt '작업 내용' [--attach] [--background] [--name 세션명]",
226
+ "",
227
+ "옵션:",
228
+ " --spawn 세션 생성 (필수)",
229
+ " --prompt TEXT Claude에 전달할 프롬프트",
230
+ " --attach WT split-pane으로 attach",
231
+ " --background attach 없이 실행, 완료 시 결과 출력",
232
+ " --name NAME 세션 이름 (기본: tfx-isolated-{ts})",
233
+ "",
234
+ ].join("\n"),
235
+ );
152
236
  process.exit(0);
153
237
  }
154
238
 
155
239
  if (!hasPsmux()) {
156
- process.stderr.write("ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n");
240
+ process.stderr.write(
241
+ "ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n",
242
+ );
157
243
  process.exit(1);
158
244
  }
159
245
 
@@ -174,10 +260,15 @@ async function main() {
174
260
  process.stdout.write(`[session-spawn] 백그라운드 대기 중...\n`);
175
261
  const result = await waitForCompletion(sessionName);
176
262
  const preview = (result.output || "(no output)").slice(0, 200);
177
- process.stdout.write(`[session-spawn] 완료: ${sessionName} | expired=${result.expired} | preview=${preview}\n`);
263
+ process.stdout.write(
264
+ `[session-spawn] 완료: ${sessionName} | expired=${result.expired} | preview=${preview}\n`,
265
+ );
178
266
  }
179
267
  }
180
268
 
181
269
  if (process.argv[1]?.endsWith("session-spawn-helper.mjs")) {
182
- main().catch((e) => { process.stderr.write(`${e.message}\n`); process.exit(1); });
270
+ main().catch((e) => {
271
+ process.stderr.write(`${e.message}\n`);
272
+ process.exit(1);
273
+ });
183
274
  }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-stale-cleanup.mjs — SessionStart 훅
4
+ *
5
+ * 새 세션 시작 시 이전 세션의 stale 상태를 정리한다:
6
+ * 1. tfx-multi-state.json — 세션 간 상태 누수 방지 (#62)
7
+ * 2. tfx-route-*-pids — 고아 워커 프로세스 정리 (#62 후속)
8
+ *
9
+ * @see scripts/headless-guard.mjs — 상태 소비자
10
+ * @see scripts/tfx-gate-activate.mjs — 상태 생산자 (ownerPid 기록)
11
+ * @see scripts/tfx-route.sh — PID tracking 파일 생산자
12
+ */
13
+
14
+ import { execSync } from "node:child_process";
15
+ import {
16
+ existsSync,
17
+ readFileSync,
18
+ readdirSync,
19
+ unlinkSync,
20
+ } from "node:fs";
21
+ import { platform, tmpdir } from "node:os";
22
+ import { join } from "node:path";
23
+
24
+ const MULTI_STATE_FILE = join(tmpdir(), "tfx-multi-state.json");
25
+ const EXPIRE_MS = 30 * 60 * 1000; // 30분
26
+ const PID_FILE_RE = /^tfx-route-(\d+)-pids$/;
27
+
28
+ function isProcessAlive(pid) {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function treeKill(pid) {
38
+ try {
39
+ if (platform() === "win32") {
40
+ execSync(`taskkill /T /F /PID ${pid}`, {
41
+ stdio: "ignore",
42
+ timeout: 5000,
43
+ windowsHide: true,
44
+ });
45
+ } else {
46
+ process.kill(pid, "SIGTERM");
47
+ }
48
+ } catch {
49
+ /* already dead */
50
+ }
51
+ }
52
+
53
+ // ── 1. tfx-multi-state.json 정리 ──
54
+ function cleanupMultiState() {
55
+ if (!existsSync(MULTI_STATE_FILE)) return;
56
+
57
+ let state;
58
+ try {
59
+ state = JSON.parse(readFileSync(MULTI_STATE_FILE, "utf8"));
60
+ } catch {
61
+ try { unlinkSync(MULTI_STATE_FILE); } catch { /* ignore */ }
62
+ return;
63
+ }
64
+
65
+ if (state.ownerPid && isProcessAlive(state.ownerPid)) return;
66
+
67
+ if (!state.ownerPid && state.activatedAt) {
68
+ if (Date.now() - state.activatedAt < EXPIRE_MS) return;
69
+ }
70
+
71
+ if (state.active) {
72
+ console.error(
73
+ `[session-stale-cleanup] stale tfx-multi state 정리 (pid=${state.ownerPid || "unknown"}, dispatched=${state.dispatched}, calls=${state.nativeWorkCalls || 0})`,
74
+ );
75
+ }
76
+
77
+ try { unlinkSync(MULTI_STATE_FILE); } catch { /* ignore */ }
78
+ }
79
+
80
+ // ── 2. orphan PID tracking 파일 정리 ──
81
+ function cleanupOrphanPidFiles() {
82
+ let files;
83
+ try {
84
+ files = readdirSync(tmpdir());
85
+ } catch {
86
+ return;
87
+ }
88
+
89
+ for (const f of files) {
90
+ const m = PID_FILE_RE.exec(f);
91
+ if (!m) continue;
92
+
93
+ const ownerPid = Number(m[1]);
94
+ if (isProcessAlive(ownerPid)) continue; // 세션 살아있음, 건드리지 않음
95
+
96
+ const filePath = join(tmpdir(), f);
97
+ try {
98
+ const pids = readFileSync(filePath, "utf8")
99
+ .split("\n")
100
+ .map((l) => l.trim())
101
+ .filter(Boolean)
102
+ .map(Number);
103
+
104
+ for (const pid of pids) {
105
+ if (pid > 0 && isProcessAlive(pid)) {
106
+ console.error(`[session-stale-cleanup] orphan worker kill: pid=${pid} (from ${f})`);
107
+ treeKill(pid);
108
+ }
109
+ }
110
+ } catch {
111
+ /* 읽기 실패 무시 */
112
+ }
113
+
114
+ try { unlinkSync(filePath); } catch { /* ignore */ }
115
+ }
116
+ }
117
+
118
+ function main() {
119
+ cleanupMultiState();
120
+ cleanupOrphanPidFiles();
121
+ }
122
+
123
+ main();